Docker + Compose

The recommended self-hosted setup: one Gooseplicant container that only your authenticating reverse proxy can reach, with its database and uploads on a Docker volume.

How sign-in works here. Gooseplicant never sees passwords. Your proxy signs the user in and forwards their email in a header. Gooseplicant trusts that header only when a second header carries the shared PROXY_SECRET, so someone who reaches the container directly can't impersonate a user.

What you need#

  • Docker with BuildKit and Docker Compose v2. The image builds from the kit, on top of node:26-slim.
  • A hostname for Gooseplicant (for example ats.example.com) pointing at the machine, ports 80 and 443 open for the proxy.
  • An identity provider your proxy can use. The example uses Authelia with Caddy; oauth2-proxy and others work too.

Install#

  1. Download, verify and unpack the kit
    Terminal
    curl -LO https://gooseplicant.goosenotes.dev/downloads/0.3.0/gooseplicant-0.3.0.tar.gz
    curl -LO https://gooseplicant.goosenotes.dev/downloads/0.3.0/SHA256SUMS
    grep ' gooseplicant-0.3.0.tar.gz$' SHA256SUMS | shasum -a 256 -c
    tar -xzf gooseplicant-0.3.0.tar.gz
    cd gooseplicant-0.3.0
  2. Build the image

    From the root of the unpacked kit (the image needs the whole workspace). The UI is compiled inside the build; there is no server build step because Node.js runs the TypeScript sources directly.

    Terminal
    docker build -f apps/node/Dockerfile -t gooseplicant:0.3.0 -t gooseplicant:latest .
  3. Create a deployment folder with the Compose file
    Terminal
    mkdir -p ~/ats-deploy
    cp apps/node/docker-compose.example.yml ~/ats-deploy/docker-compose.yml
    cd ~/ats-deploy
  4. Generate the two secrets

    Compose reads them from .env next to docker-compose.yml. Keep a copy of ATS_INGEST_SECRET: your website needs the same value.

    Terminal
    printf 'ATS_INGEST_SECRET=%s\nATS_PROXY_SECRET=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
    chmod 600 .env
  5. Edit the ats service

    In docker-compose.yml check that image is the tag you just built (gooseplicant:latest), set PUBLIC_URL to your public origin and BOOTSTRAP_ADMINS to your email. Uncomment MAIL_FROM and SMTP_URL if you want email, and add EDITION: lite if you only want Gosling. The rest of the example already fits this setup:

    docker-compose.yml (ats service)
    services:
      ats:
        image: gooseplicant:latest
        restart: unless-stopped
        environment:
          PUBLIC_URL: https://ats.example.com
          BASE_PATH: /ats
          INGEST_SECRET: ${ATS_INGEST_SECRET:?set in .env}
          PROXY_SECRET: ${ATS_PROXY_SECRET:?set in .env}
          PROXY_EMAIL_HEADER: x-forwarded-email
          BOOTSTRAP_ADMINS: you@example.com
          TRUST_PROXY: "true"
          # EDITION: lite                  # Gosling only; default is full
          # MAIL_FROM: careers@example.com
          # SMTP_URL: smtps://user:password@smtp.example.com:465
        volumes:
          - ats-data:/data
        expose:
          - "3000"
        networks: [internal]

    Note expose, not ports: the container is reachable on the internal network only. All variables are listed in Configuration.

    The image declares two ports, EXPOSE 3000 3001. 3000 is the app; 3001 is used only when you set INGEST_PORT=3001 for an intake-only listener, which lets you publish applications from your website without publishing the app. See Public intake, private app.

  6. Write the Caddyfile

    Create Caddyfile next to docker-compose.yml. Requests to /ats/ingest skip the login (they are HMAC-signed, server to server); everything else under /ats goes through Authelia, which supplies the email, and Caddy adds the shared secret.

    Caddyfile
    ats.example.com {
      handle /ats/ingest {                      # server-to-server, HMAC-signed: no user login
        reverse_proxy ats:3000
      }
      handle /ats* {
        forward_auth authelia:9091 {
          uri /api/authz/forward-auth
          copy_headers Remote-Email>X-Forwarded-Email Remote-Name
        }
        reverse_proxy ats:3000 {
          header_up X-ATS-Proxy-Secret {env.ATS_PROXY_SECRET}
        }
      }
    }
  7. Add your Authelia service

    The example Compose file references authelia:9091 but does not define it. Add an authelia service on the same internal network, configured for your users or identity provider, following Authelia's own documentation. Any proxy that authenticates users and adds the two headers works instead; see other proxies.

  8. Start it
    Terminal
    docker compose up -d
    docker compose ps                # the ats service turns "healthy" once it answers /ats/api/health
    docker compose logs -f ats       # one JSON object per line

    Configuration is validated at startup: if anything is missing or invalid, every problem is listed by variable name and the container exits before opening a port.

  9. Sign in and invite your team

    Open https://ats.example.com/ats/, sign in as a bootstrap admin, then add people under Settings → Users & access. Gosling is at /ats/lite/. Next, connect your form.

Proxy requirements#

Whatever you put in front, it must:

  1. authenticate the user and set X-Forwarded-Email (or your PROXY_EMAIL_HEADER), overwriting anything the client sent;
  2. add X-ATS-Proxy-Secret: $PROXY_SECRET to the requests it forwards;
  3. not require a login for /ats/ingest;
  4. be the only thing that can reach the Gooseplicant port. Never publish it.

Want the HR app reachable only inside your network? Publish the intake alone through a tunnel and keep the proxy on the LAN: Public intake, private app.

With TRUST_PROXY=true, Gooseplicant takes the client IP for the audit log from the right-most X-Forwarded-For entry, so the proxy must append to that header.

Other proxies#

  • Authelia sends Remote-Email. Either copy it to X-Forwarded-Email as above, or set PROXY_EMAIL_HEADER=remote-email.
  • oauth2-proxy: use --pass-user-headers when it proxies directly, or --set-xauthrequest with nginx auth_request. Inject X-ATS-Proxy-Secret in the fronting proxy, and exempt /ats/ingest from authentication. An nginx sketch is in the Node.js guide.
  • Display names: set PROXY_NAME_HEADER (for example remote-name or x-forwarded-preferred-username) to show names instead of emails.

Without Compose#

If your proxy already runs on the host, a single container bound to loopback works too:

Terminal
docker run -d --name gooseplicant --restart unless-stopped \
  -v ats-data:/data --env-file ats.env \
  -p 127.0.0.1:3000:3000 gooseplicant:latest

Keep comments on their own lines in env files. docker run --env-file does not strip a trailing # comment; it becomes part of the value.

Volumes and data#

VolumeMounted atHolds
ats-data/dataats.db (SQLite in WAL mode, with its -wal/-shm files) and files/ (uploaded resumes and attachments). This is everything to back up.
caddy-data/data in the proxyCaddy's certificates and state.

The container runs as the non-root node user. On SIGTERM (docker compose stop) it stops accepting requests, waits for an in-flight scheduled run and closes the database.

Backups#

Snapshot the database with SQLite's online backup (safe while Gooseplicant runs) and archive the files folder. Don't copy ats.db alone: in WAL mode recent writes live in the -wal file. Compose prefixes volume names with the project (folder) name; check yours with docker volume ls.

Terminal
mkdir -p backups
docker run --rm -v ats-deploy_ats-data:/data -v "$PWD/backups":/backup alpine:3 sh -c '
  apk add --no-cache sqlite >/dev/null &&
  sqlite3 /data/ats.db ".backup /backup/ats-$(date +%F).db" &&
  tar -czf /backup/files-$(date +%F).tar.gz -C /data files'

To restore, stop the service, put the snapshot back as ats.db (removing any ats.db-wal/ats.db-shm), unpack files/, and give everything back to the node user (uid 1000):

Terminal
docker compose stop ats
docker run --rm -v ats-deploy_ats-data:/data -v "$PWD/backups":/backup alpine:3 sh -c '
  rm -f /data/ats.db-wal /data/ats.db-shm &&
  cp /backup/ats-2026-01-31.db /data/ats.db &&
  rm -rf /data/files && tar -xzf /backup/files-2026-01-31.tar.gz -C /data &&
  chown -R 1000:1000 /data'
docker compose start ats

Upgrading#

Back up, unpack the new kit, rebuild the image and recreate the container; the volume is kept and database migrations run automatically on start.

Terminal
cd gooseplicant-0.3.0                               # the NEW kit
docker build -f apps/node/Dockerfile -t gooseplicant:0.3.0 -t gooseplicant:latest .
cd ~/ats-deploy && docker compose up -d     # recreates ats with the new image

Details and rollback advice: Upgrading.