Skip to content

Forgejo in Compose: Built-in SSH, Push-to-Create, and LFS

Adityo Guni Waluyo

One Compose service driven by FORGEJO__ env vars: built-in loopback SSH, push-to-create, LFS, and a healthcheck that tells the truth.

This afternoon I pushed a revert commit to my agent repo: the Forgejo config I had just used to open LAN access got closed again, back to loopback-only. Slightly odd indeed, breaking it and tidying it back within one hour. But that is exactly the point. Yesterday I covered why I need Forgejo as a secondary git host; today is the more practical part: the entire setup is just 20 lines of environment in Compose, no separate config file, no SSH proxy on the host. And it turns out the container's built-in SSH alone is enough, no need to expose HTTP to the network at all.

Built-in SSH: Tell the Advertised Port Apart From the Listen Port

My first mistake was still thinking in the old pattern: host SSH, inject keys, proxy to the container. Hassle. Forgejo ships a built-in SSH server that you enable with one environment variable, FORGEJO__server__START_SSH_SERVER=true. What confused me at first: two ports that look alike but do different jobs. SSH_LISTEN_PORT is the port where the built-in SSH server actually listens inside the container [3]. SSH_PORT is the port advertised in the clone URLs shown in the web UI [3]. My container listens on 22 (its internal port), but what gets advertised is 3122, because on the host, 3122 is the published port.

  forgejo:
    image: codeberg.org/forgejo/forgejo:16
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      FORGEJO__server__DISABLE_SSH: "false"
      FORGEJO__server__START_SSH_SERVER: "true"
      FORGEJO__server__SSH_LISTEN_PORT: "22"
      FORGEJO__server__SSH_PORT: "3122"
      FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true"
      FORGEJO__lfs__LFS_START_SERVER: "true"
    ports:
      - "3111:3000"  # HTTP, host loopback bind only
      - "3122:22"    # built-in SSH, host loopback bind
    volumes:
      - ./forgejo-data:/data

The easy-to-miss detail: this container image reads env variables in the FORGEJO__[SECTION]__[KEY] format and writes them into app.ini [1], so the whole config above is official and documented, not a dark trick. The dark trick hides somewhere else: forgetting to set USER_UID and USER_GID to match the ownership of the volume folder. The docs are explicit, with wrong volume permissions the container may refuse to start [1]. I use 1000:1000 with a forgejo-data bind mount. A bonus I like: with no database configuration, Forgejo falls straight back to SQLite [1], so a personal forge needs no extra Postgres service.

Push-to-Create: New Repositories Without Opening the Web UI

The feature that changed my workflow the most is push-to-create: git push to a repo URL that does not exist yet, and the repo gets created on the spot. The feature ships disabled [2]. For a single-user forge like mine, one line, FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true, a restart, done. The docs are honest about the requirement too: this is not an anonymous door. Whoever pushes must already be authenticated as a user, and the repo is created under that user's name [2].

One consequence to be aware of before using it: the visibility of a push-to-create repo follows DEFAULT_PUSH_CREATE_PRIVATE, which defaults to private [3]. Want it public right away? Push options do it without touching the UI: git push -o repo.private=false -u origin main [2]. For me the private default is exactly right; private backups do not need a re-declaration every time I create a repo.

LFS and the Healthcheck: Two Lines Often Treated as Optional

The last two lines are, in my opinion, mandatory from day one, not later. First FORGEJO__lfs__LFS_START_SERVER=true, because the default is off [3] and without it the LFS server never comes up, while the entire reason I moved big files to Forgejo is its LFS. The quota is the disk bind mount itself, so the size of the forgejo-data folder is the real limit [1]. Second, the healthcheck. I once pointed wget at the front page and got a healthy status while the forge was not even fully installed. The /api/healthz endpoint exists precisely for this case: it is registered on the install routes so a Docker healthcheck can still work before the instance is fully ready [4]. My compose healthcheck now wgets that path over the container loopback, every 30 seconds.

About this afternoon's revert: the call was right. HTTP stays loopback-only, laptop access goes through SSH on 3122, and Forgejo pulls all the work. Eight lines of env to maintain, all readable on one screen, and not a single one I need to memorize twice.

Sumber

  1. Forgejo Docs: Installation with Docker
  2. Forgejo Docs: Push To Create
  3. Forgejo Docs: Configuration Cheat Sheet
  4. Forgejo Commit e933f31: Add health check endpoint

Related articles