What the compose file runs
The repository's docker-compose.yml defines the four containers the install docs list. Both Activepieces services use one image, pinned to a release tag (0.92.2 in the copy read on 23 October 2026), and differ only in AP_CONTAINER_TYPE.
| Service | Image | Job | Exposed |
|---|---|---|---|
| app | ghcr.io/activepieces/activepieces | API and web UI, port 80 in the container | 8080:80 as shipped |
| worker | same image | Runs flows; replicas: 5 | nothing |
| postgres | pgvector/pgvector:0.8.0-pg14 | Flows, runs, connections | nothing |
| redis | redis:7.0.7 | Job queue | nothing |
Flows live in the postgres_data volume, not in the checkout folder. With the default AP_FILE_STORAGE_LOCATION=DB, files that steps produce are stored in the database as well.
Sizing the machine
These are the docs' figures with their scope. Production Setup sizes a split fleet with managed Postgres, Redis and S3; on one machine read them as per-service guides.
| What | Docs figure | Stated on |
|---|---|---|
| Whole compose install | at least 2 vCPU and 4 GB RAM | Self Host Activepieces |
| Worker, concurrency 1 | 0.5 vCPU / 1 GB | Production Setup |
| Worker, concurrency 5 (default) | about 2.5 vCPU / 5 GB | Production Setup, migration table |
| App | 1 vCPU / 1 GB | Production Setup |
| Postgres | 2 vCPU / 4 GB, a starting point | Production Setup |
| Redis | 1 vCPU / 1 GB | Production Setup |
Our arithmetic: five replicas at the default AP_WORKER_CONCURRENCY=5 make 25 job slots, about 12.5 vCPU and 25 GB at that per-worker figure. The install page calls five replicas more than a small machine wants, so set replicas to what the box holds.
On the dedicated server range, the single- and dual-socket rack lines fit: one tenant, all the memory, and a full out-of-band console on every racked single-, dual- and quad-socket server, useful when a firewall rule cuts SSH. A cartridge with 16 GB or more carries a small install; of the cartridges, just the m510 and m710x have a console. The X-Gene ARM cartridge is out too, as it boots nothing newer than Ubuntu 18.04. Racked servers are handed over in about 30 minutes once payment is approved; a build to order takes 4 to 24 hours.
Installing the stack
This is the docs' manual route (clone, tools/deploy.sh, compose up) on Ubuntu as root, firewall first. The worker override AP_FRONTEND_URL=http://app is the install page's own step. Our changes are two: the app port bound to 127.0.0.1, and umask 077 so .env is born at mode 600 before deploy.sh writes secrets into it. We leave out the docs' edition step (AP_EDITION=ee with AP_EXECUTION_MODE=SANDBOX_CODE_ONLY), so this install is Community; see the licence section before your first start if you may buy a key.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
# Docker Engine 28+ with the Compose v2 plugin, per docs.docker.com
apt install -y git openssl
git clone --depth 1 https://github.com/activepieces/activepieces.git /opt/activepieces
cd /opt/activepieces
(umask 077 && sh tools/deploy.sh)
sed -i "s/'8080:80'/'127.0.0.1:8080:80'/" docker-compose.yml
sed -i '/- AP_CONTAINER_TYPE=WORKER/a\ - AP_FRONTEND_URL=http://app' docker-compose.yml
# starts only if every check passes
[ "$(stat -c %a .env)" = 600 ] \
&& [ "$(grep -cE '^(AP_ENCRYPTION_KEY|AP_JWT_SECRET|AP_POSTGRES_PASSWORD)=.' .env)" = 3 ] \
&& grep -q "'127.0.0.1:8080:80'" docker-compose.yml \
&& grep -q 'AP_FRONTEND_URL=http://app' docker-compose.yml \
&& docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d
curl -s http://127.0.0.1:8080/api/v1/health
Compose starts only when all four checks hold: .env at mode 600, three non-empty secrets (the docs warn deploy.sh reports success with blank values when openssl is missing), the 127.0.0.1 bind, and the worker override. If the health call gets no answer, nothing started: find the failed check, fix it, and run the chain again. deploy.sh hands each generated secret to sed on its command line for an instant, where ps can show it, so run it on a box nobody else is logged into.
Why the bind: a mapping without an address publishes on every host address (0.0.0.0 and [::]). Published traffic skips ufw's rules on IPv4, and also on IPv6 once Docker's own IPv6 networking is enabled; otherwise the [::] listener is docker-proxy, and ufw filters that. Mapping to 127.0.0.1 is local-only on Docker Engine 28 and later (moby#45610). Once the stack runs, anyone allowed to use docker inspect can read the secrets from .env in the live containers' environment.
Admin account, sign-ups and execution mode
Per the install page, the earliest account on a fresh instance gets platform administrator rights, and no default credentials exist. Do that before anything is public, through a tunnel from your workstation: ssh -L 8080:127.0.0.1:8080 -N admin@203.0.113.10, then browse to http://localhost:8080, the AP_FRONTEND_URL that .env.example ships.
From then on the platform is invite-only by default: authentication.service.ts routes a sign-up to an existing platform through an invitation check unless AP_ALLOW_OPEN_SIGN_UP is true, and the 0.88.2 breaking-changes entry says the same. Leave it unset and invite people from Platform Admin, Users.
.env.example ships AP_EXECUTION_MODE=UNSANDBOXED. The docs' table of modes:
| Mode | npm in Code steps | Privileged Docker | Multi-tenant safe |
|---|---|---|---|
UNSANDBOXED | yes | no | no |
SANDBOX_CODE_ONLY (V8 isolates, 128 MB each) | no | no | yes |
SANDBOX_PROCESS (isolate, kernel namespaces) | yes | yes | yes |
SANDBOX_CODE_AND_PROCESS | no | yes | yes |
The docs suggest no sandboxing for single-tenant use and SANDBOX_CODE_ONLY for multi-tenant. Our reading for one box: an unsandboxed Code step runs inside the worker container, on the same Docker network as Redis and Postgres, so sandbox if people you do not fully trust write flows. AP_NETWORK_MODE=STRICT also blocks user code from private, loopback, link-local and metadata addresses, which the docs call best-effort, in-process protection. Change either in .env, then recreate app and worker.
Public URL, nginx and webhooks
AP_FRONTEND_URL builds webhook and redirect URLs, and the environment reference says third parties must reach it for webhooks and app triggers to work, which rules out an IP allow-list on the proxy. Order: DNS, then the certificate, then the address in .env. Only the location block is the docs' HTTPS example, with its websocket upgrade headers. The port-80 server around it, which certbot then extends, is ours, and so is 127.0.0.1 in place of localhost, since the app listens on IPv4 loopback only.
# 1. DNS: A and AAAA records for flows.example.com resolve to this server
ufw allow 80,443/tcp
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/activepieces <<'EOF'
server {
listen 80;
listen [::]:80;
server_name flows.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
EOF
ln -s /etc/nginx/sites-available/activepieces /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# 2. Certificate: certbot adds the 443 block and the redirect
certbot --nginx -d flows.example.com --redirect -m you@example.com --agree-tos --no-eff-email
# 3. Only now: the public address, then recreate app and worker
sed -i 's|^AP_FRONTEND_URL=.*|AP_FRONTEND_URL=https://flows.example.com|' /opt/activepieces/.env
docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d --force-recreate app worker
On Ubuntu 24.04, nginx comes from the main component; certbot and python3-certbot-nginx come from universe. The three flags answer certbot's questions; without them it asks for an email address for renewal notices, asks you to accept the Let's Encrypt terms, and asks whether to share that address with the EFF. A dead Test Flow button or missing live updates point at missing upgrade headers, per the docs' websocket page.
Backups and a live restore
The install page is blunt: without AP_ENCRYPTION_KEY, stored connections cannot be decrypted even from a full database backup. Each run therefore keeps the dump, .env and the edited compose file together. Redis is left out on purpose: the Disaster Recovery page says not to back it up, since schedules and timers are rebuilt from Postgres.
install -d -m 700 /var/backups/activepieces
install -m 700 /dev/null /opt/activepieces/backup.sh
cat > /opt/activepieces/backup.sh <<'EOF'
set -euo pipefail
umask 077
d=/var/backups/activepieces; t=$(date +%F)
c="docker compose -p activepieces -f /opt/activepieces/docker-compose.yml"
$c exec -T postgres pg_dump -U postgres -Fc activepieces > $d/db-$t.dump
$c exec -T postgres pg_restore -l < $d/db-$t.dump > /dev/null
cp /opt/activepieces/.env $d/env-$t
cp /opt/activepieces/docker-compose.yml $d/compose-$t.yml
EOF
printf '%s\n' \
'15 3 * * * root umask 077; bash /opt/activepieces/backup.sh > /var/backups/activepieces/last.log 2>&1' \
'45 3 * * * root rsync -a /var/backups/activepieces/ backup@backup.example.net:activepieces/ > /root/ap-push.log 2>&1' \
> /etc/cron.d/activepieces-backup
The push needs root's own key first. As root, ssh-keygen -t ed25519 prompts for a file location (accept the default) and a passphrase twice (empty, for an unattended job); ssh-copy-id backup@backup.example.net then shows the host key fingerprint for a yes, and asks for the remote password. Prune old copies on both ends yourself.
Live restore, after defining alias dc="docker compose -f /opt/activepieces/docker-compose.yml -p activepieces":
- Copy the chosen dump in and test it:
dc cp /var/backups/activepieces/db-DATE.dump postgres:/tmp/restore.dump, thendc exec -T postgres pg_restore -l /tmp/restore.dumpmust print a table of contents. dc stop app worker.- Set the current data aside and test that copy:
dc exec -T postgres pg_dump -U postgres -Fc -f /tmp/pre-restore.dump activepieces,dc exec -T postgres pg_restore -l /tmp/pre-restore.dump, thendc cp postgres:/tmp/pre-restore.dump /var/backups/activepieces/. Stop here unless both commands succeeded. dc exec -T postgres dropdb -U postgres activepieces,dc exec -T postgres createdb -U postgres activepieces,dc exec -T postgres pg_restore -U postgres -d activepieces /tmp/restore.dump.- Check
.envholds theAP_ENCRYPTION_KEYsaved with that dump,dc up -d, call the health URL, sign in. - To roll back, run step 4 against
/tmp/pre-restore.dump. Delete the set-aside copy only once the restored data checks out.
Restoring to a new machine: put the saved .env and compose file in place before the postgres container first starts, because the image creates its user and password from that environment on first start only. Then dc up -d postgres, steps 1 and 4, and dc up -d.
Moving to a new release
The image tag is pinned in docker-compose.yml, and the install page leaves bumping it to you. Run the backup script, then read the breaking-changes list for every release you cross; 0.88.2, for one, dropped PM2 from the image, so a crash now exits the container and the shipped restart: unless-stopped starts it again.
- Pick a release on the GitHub releases page and set
Vto its tag. - In
/opt/activepieces:sed -i -E "s|(ghcr.io/activepieces/activepieces:).*|\1$V|" docker-compose.yml;grep -c "activepieces:$V" docker-compose.ymlmust print 2. dc pull,dc up -d, then the health URL.
Going back: the Rollback page gives a command that runs in the newer image to reverse migrations, and flags releases whose migrations cannot be reversed. On one box, the restore above with the old tag back in place is the plainer route.
Licence and editions
The repository's LICENSE puts everything outside packages/ee/ and packages/server/api/src/app/ee under the MIT (Expat) licence. Those two folders fall under packages/ee/LICENSE, the Activepieces Enterprise licence: production use only with a valid Enterprise licence, copying and modifying allowed for development and testing. The docs' licence page says self-hosted use of the enterprise features requires an Activepieces licence.
Our general reading, not legal advice: MIT asks you to keep the copyright notice and licence text in copies you pass on, and does not oblige you to publish source. The edition defaults to Community in system.ts. Setting AP_EDITION=ee with AP_EXECUTION_MODE=SANDBOX_CODE_ONLY (ee rejects UNSANDBOXED) is an ordinary step of the manual install, one this page skips; without it you get Community and cannot activate a licence key later. If you may buy one, add both lines to .env before the first compose up.
When one machine is the wrong shape
- Peak concurrency in the hundreds: the docs' production shape is a fleet of concurrency-1 workers with managed Postgres, Redis and S3, and they call S3 a hard requirement for their throughput figures.
- Untrusted users who need npm in Code steps: that means
SANDBOX_PROCESSand a privileged worker, which the docs say should not share a node with unrelated workloads. - Async webhooks that must never be lost: they wait in Redis before Postgres has a record, so Redis persistence sets what a failure can lose.
Comparing tools? See n8n on a dedicated server. To give the database its own box later, see PostgreSQL on a dedicated server.
Questions
Does the Community edition cost anything to self-host?
The MIT-licensed code carries no licence fee, and Community is the default edition. Features in the two ee folders need a paid Activepieces licence for production use. What remains is the machine and your time.
Why do webhook URLs point at localhost?
Activepieces builds them from AP_FRONTEND_URL, and .env.example ships http://localhost:8080. Set the public HTTPS address once the proxy and certificate work, then recreate app and worker.
Is the single-container hobbyist install enough?
The docs pitch it at personal use on one machine, with embedded PGLite and in-memory Redis, and say it cannot scale, cannot move to Enterprise, and leaving it means starting over. For a team server they recommend the compose route.
How many flows run at the same time?
Each worker takes AP_WORKER_CONCURRENCY jobs at once (default 5) and the shipped file starts five workers. Each project is also capped by AP_DEFAULT_CONCURRENT_JOBS_LIMIT, default 5, which can be overridden per project.