Skip to Content

Production Guide: Deploy Wakapi with Docker Compose + Caddy + PostgreSQL on Ubuntu

Self-host a private, WakaTime-compatible coding activity tracker with unlimited history.

Developers who care about productivity and continuous improvement need accurate data on where their coding time actually goes. WakaTime has popularized this idea, but its free tier limits history to two weeks and all data flows through a third-party service — an uncomfortable trade-off for teams handling proprietary code. Wakapi solves both problems: it is a self-hosted, open-source, WakaTime-compatible coding activity tracker that stores your data locally, retains unlimited history, and accepts the same editor plugins WakaTime uses, requiring only a one-line config change to point them at your own server.

This guide covers a production-ready deployment of Wakapi on Ubuntu using Docker Compose and Caddy for automatic HTTPS. You will end up with a private dashboard tracking time spent per language, editor, project, and OS — data that lives entirely on your own infrastructure.

Architecture and flow overview

The stack has three components. Wakapi runs as a Go binary packaged in a lightweight Docker image, listening on port 3000. It stores all activity data in a PostgreSQL database for durability, transactionality, and efficient range queries on time-series activity records. Caddy sits in front as the TLS-terminating reverse proxy, automatically issuing and renewing a Let's Encrypt certificate for your chosen subdomain.

Traffic flow: editor plugin heartbeat (HTTPS) → Caddy (port 443) → Wakapi container (port 3000, Docker bridge network) → PostgreSQL container (port 5432, internal only). Both application containers share a user-defined bridge network. PostgreSQL is never exposed to the host. Caddy rewrites requests transparently so existing WakaTime-compatible plugins see a standard REST API surface with no modifications beyond the base URL and API key.

Session data is persisted in named Docker volumes: one for the PostgreSQL data directory and one for the Caddy TLS certificates. This means a docker compose down followed by docker compose up -d restores the full state including all historical records and the existing HTTPS certificate.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with a public IP address
  • A DNS A record pointing your subdomain (e.g., wakapi.example.com) to the server IP — fully propagated before starting
  • Docker Engine 24+ and Docker Compose v2 installed (docker compose plugin, not the legacy binary)
  • Ports 80 and 443 open in your firewall (ufw allow 80/tcp && ufw allow 443/tcp)
  • At least 512 MB of free RAM; Wakapi and PostgreSQL together consume well under 256 MB at idle
  • A non-root sudo user or root shell for initial setup

Step-by-step deployment

1. Create the project directory

mkdir -p /opt/wakapi && cd /opt/wakapi

2. Create the environment file

Place secrets in a .env file rather than hard-coding them into the Compose file. Restrict file permissions immediately after creation.

cat > /opt/wakapi/.env << 'EOF'
POSTGRES_DB=wakapi
POSTGRES_USER=wakapi
POSTGRES_PASSWORD=changeme_strong_password
WAKAPI_PASSWORD_SALT=changeme_random_salt_32chars
WAKAPI_ADMIN_PASSWORD=changeme_admin_password
EOF
chmod 600 /opt/wakapi/.env

3. Write the Docker Compose file

services:
  db:
    image: postgres:16-alpine
    container_name: wakapi-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - wakapi_db:/var/lib/postgresql/data
    networks:
      - internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  wakapi:
    image: ghcr.io/muety/wakapi:latest
    container_name: wakapi
    restart: unless-stopped
    env_file: .env
    environment:
      WAKAPI_DB_TYPE: postgres
      WAKAPI_DB_HOST: db
      WAKAPI_DB_PORT: 5432
      WAKAPI_DB_NAME: ${POSTGRES_DB}
      WAKAPI_DB_USER: ${POSTGRES_USER}
      WAKAPI_DB_PASSWORD: ${POSTGRES_PASSWORD}
      WAKAPI_PASSWORD_SALT: ${WAKAPI_PASSWORD_SALT}
      WAKAPI_ALLOW_SIGNUP: "false"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - internal
      - web

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web
    depends_on:
      - wakapi

networks:
  internal:
    driver: bridge
  web:
    driver: bridge

volumes:
  wakapi_db:
  caddy_data:
  caddy_config:

4. Write the Caddyfile

Replace wakapi.example.com with your actual subdomain.

wakapi.example.com {
    reverse_proxy wakapi:3000
    encode gzip
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options SAMEORIGIN
        Referrer-Policy strict-origin-when-cross-origin
    }
}

5. Start the stack

cd /opt/wakapi
docker compose up -d

Watch the Caddy logs for the ACME certificate challenge and the Wakapi logs for the database migration to complete:

docker compose logs -f

Wakapi prints Starting server on port 3000 when it is ready. Caddy prints certificate obtained successfully once TLS is provisioned. Both typically complete within 60 seconds.

6. Create the admin user

Open https://wakapi.example.com in a browser. Sign up is disabled by default (WAKAPI_ALLOW_SIGNUP=false), so the first user must be created via the Wakapi CLI:

docker exec -it wakapi ./wakapi --create-admin \
  --admin-user admin \
  --admin-password "$WAKAPI_ADMIN_PASSWORD"

Log in at the web UI with those credentials. Navigate to Settings → API Key to copy your personal API key — you will paste this into your editor plugin.

7. Configure your editor plugin

Any WakaTime-compatible plugin (VS Code, JetBrains IDEs, Neovim, Emacs, Sublime Text) works with Wakapi. The only required change from the standard WakaTime setup is setting a custom API URL. In VS Code with the WakaTime extension, open the command palette and run WakaTime: Settings, then set:

api_url = https://wakapi.example.com/api
api_key = <your-wakapi-api-key>

For Neovim, Vim, or Emacs, update ~/.wakatime.cfg on each developer machine:

[settings]
api_url = https://wakapi.example.com/api
api_key = <your-wakapi-api-key>

Configuration and secrets handling

All sensitive values live in /opt/wakapi/.env with permissions set to 600. The Compose file references them via variable substitution so no secrets appear in docker inspect output for the container configuration. Rotate the POSTGRES_PASSWORD by updating the .env file, updating the PostgreSQL user password inside the database, and restarting the Wakapi container:

docker exec -it wakapi-db psql -U wakapi -c \
  "ALTER USER wakapi WITH PASSWORD 'new_strong_password';"
# Update .env with the new password, then:
docker compose up -d wakapi

The WAKAPI_PASSWORD_SALT is used to hash user passwords stored in the database. Do not change this value after users have been created — doing so will invalidate all existing password hashes. Store a backup copy of this salt in a secrets manager or vault alongside your PostgreSQL credentials.

To invite additional users without opening public sign-up, enable it temporarily, create the accounts, then disable it again:

# Temporarily enable sign-up
docker compose exec wakapi sh -c 'WAKAPI_ALLOW_SIGNUP=true ./wakapi'
# Better: set WAKAPI_ALLOW_SIGNUP=true in .env, restart, create users, revert

For teams, prefer creating users directly via the CLI or seeding them through the Wakapi admin panel rather than ever enabling public sign-up on an internet-facing instance.

Verification

After the stack is running, verify each layer systematically:

# All three containers should show "Up"
docker compose ps

# Caddy should serve HTTPS with a valid certificate
curl -I https://wakapi.example.com

# Wakapi API health check
curl https://wakapi.example.com/api/health
# Expected: {"status":"OK"}

# Verify the database is accepting connections
docker exec wakapi-db pg_isready -U wakapi -d wakapi

After configuring the editor plugin on a developer machine, open a file, make some edits, and wait 30 seconds for the first heartbeat to be sent. Navigate to the Wakapi dashboard at https://wakapi.example.com and confirm you see activity appearing under Today. If the dashboard shows no data after 2 minutes of editing, check the browser developer tools Network tab for failed heartbeat requests — typically caused by a wrong API key or a misconfigured api_url missing the /api suffix.

Common issues and fixes

Wakapi container exits immediately after startup: The most common cause is a database connection failure before PostgreSQL is fully ready. The Compose healthcheck on the db service combined with depends_on: condition: service_healthy prevents this, but if you see this with an older Docker version that does not support health-check conditions, add a restart: unless-stopped to the Wakapi service — it will retry until the database is reachable.

ACME certificate challenge fails: Port 80 must be reachable from the internet for the HTTP-01 challenge. Confirm your firewall rules with ufw status and that no upstream NAT or security group is blocking port 80. Also verify the DNS A record resolves to your server's public IP using dig +short wakapi.example.com.

Editor plugin sends heartbeats but the dashboard shows no data: Verify the api_url ends with /api (not just the domain). Wakapi's WakaTime-compatible endpoint is at /api/compat/wakatime/v1, but plugins that use the standard WakaTime base URL (/api) will route correctly if the plugin constructs full paths. Check docker compose logs wakapi for incoming heartbeat log lines to confirm requests are reaching the server.

PostgreSQL data volume fills up over time: Wakapi stores raw heartbeat records. On large teams, run a periodic cleanup of old heartbeats beyond your desired retention window using the Wakapi admin API or a SQL delete against the heartbeats table. A cron-scheduled docker exec wakapi-db psql -U wakapi -c "DELETE FROM heartbeats WHERE time < NOW() - INTERVAL '1 year';" keeps the database size bounded.

Caddy container shows permission errors for the TLS data volume: This happens when the caddy_data volume was created by a prior run with a different UID. Run docker compose down -v to remove the volume (certificates will be re-issued), then docker compose up -d. Caddy re-issues a fresh certificate within seconds.

FAQ

Is Wakapi fully compatible with existing WakaTime editor plugins?

Yes. Wakapi implements the WakaTime v1 REST API surface. Any plugin that supports setting a custom api_url works without modification. This includes the official WakaTime extensions for VS Code, JetBrains IDEs, Sublime Text, Neovim, Vim, and Emacs. The only required change is setting api_url = https://your-wakapi-domain/api in the plugin configuration and replacing the WakaTime cloud API key with your Wakapi-issued key.

Can multiple developers share one Wakapi instance?

Yes. Each developer has their own account and API key. Activity data is isolated per user — one developer cannot see another's statistics unless an admin grants explicit access. Wakapi supports multi-user deployments with per-user dashboards, project filtering, and label customization. Keep WAKAPI_ALLOW_SIGNUP=false in production and create accounts for new team members via the admin CLI to avoid unauthorized registrations.

What happens to my data if the container is removed?

All persistent data lives in the wakapi_db Docker named volume. As long as you do not run docker compose down -v (which removes volumes), your data is safe across container restarts, image upgrades, and even host reboots. To migrate to a new server, export the volume with docker run --rm -v wakapi_db:/data -v /tmp:/backup alpine tar czf /backup/wakapi_db.tar.gz /data and import it on the target host before starting the stack.

How do I back up Wakapi data?

Use a standard PostgreSQL dump taken directly from the running container:

docker exec wakapi-db pg_dump -U wakapi wakapi | gzip > /backup/wakapi_$(date +%F).sql.gz

Schedule this in a daily cron job and ship the compressed dump to an S3 bucket or remote storage. Restoring is equally straightforward: gunzip the dump and pipe it into psql against a fresh database.

Can I import my historical WakaTime data into Wakapi?

Yes. Wakapi supports importing a WakaTime data export directly through the web UI under Settings → Import. Download your full history from WakaTime's Account → Export page, then upload the resulting JSON file to your self-hosted instance. This gives you continuity from your cloud-based history without losing any past records.

Does Wakapi support custom project labels or ignore patterns?

Yes. In the Wakapi web UI under Settings → Projects, you can assign aliases to merge differently named projects (e.g., myapp and my-app appear as one). Under Settings → Ignore, you can add file path patterns to exclude from tracking — useful for hiding vendor directories, generated files, or private personal projects from your statistics. These settings are per-user and take effect retroactively for the summary view, though raw heartbeat data is unchanged.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy IT-Tools with Docker Compose + Caddy on Ubuntu
Self-host a private, always-available developer toolbox with 70+ utilities behind automatic HTTPS — no accounts, no SaaS dependencies.