Use case · Workflow automation

Activepieces self hosted on a dedicated server: compose stack, admin, proxy, backups

Activepieces self hosted means four containers from the repository's own docker-compose.yml (the app, a worker service, Postgres and Redis) with secrets that tools/deploy.sh writes into .env; the install page asks for at least 2 vCPU and 4 GB of RAM. This page runs that stack on one Linux machine with the app port bound to 127.0.0.1, creates the platform admin through an SSH tunnel, then adds nginx, a certificate and the public URL that webhooks need. Backups, upgrades and the licence split follow.

See the machines

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.

ServiceImageJobExposed
appghcr.io/activepieces/activepiecesAPI and web UI, port 80 in the container8080:80 as shipped
workersame imageRuns flows; replicas: 5nothing
postgrespgvector/pgvector:0.8.0-pg14Flows, runs, connectionsnothing
redisredis:7.0.7Job queuenothing

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.

WhatDocs figureStated on
Whole compose installat least 2 vCPU and 4 GB RAMSelf Host Activepieces
Worker, concurrency 10.5 vCPU / 1 GBProduction Setup
Worker, concurrency 5 (default)about 2.5 vCPU / 5 GBProduction Setup, migration table
App1 vCPU / 1 GBProduction Setup
Postgres2 vCPU / 4 GB, a starting pointProduction Setup
Redis1 vCPU / 1 GBProduction 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:

Modenpm in Code stepsPrivileged DockerMulti-tenant safe
UNSANDBOXEDyesnono
SANDBOX_CODE_ONLY (V8 isolates, 128 MB each)nonoyes
SANDBOX_PROCESS (isolate, kernel namespaces)yesyesyes
SANDBOX_CODE_AND_PROCESSnoyesyes

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":

  1. Copy the chosen dump in and test it: dc cp /var/backups/activepieces/db-DATE.dump postgres:/tmp/restore.dump, then dc exec -T postgres pg_restore -l /tmp/restore.dump must print a table of contents.
  2. dc stop app worker.
  3. 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, then dc cp postgres:/tmp/pre-restore.dump /var/backups/activepieces/. Stop here unless both commands succeeded.
  4. 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.
  5. Check .env holds the AP_ENCRYPTION_KEY saved with that dump, dc up -d, call the health URL, sign in.
  6. 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.

  1. Pick a release on the GitHub releases page and set V to its tag.
  2. In /opt/activepieces: sed -i -E "s|(ghcr.io/activepieces/activepieces:).*|\1$V|" docker-compose.yml; grep -c "activepieces:$V" docker-compose.yml must print 2.
  3. 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_PROCESS and 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.

A machine for your flows

Choose a rack line, install Ubuntu 24.04 and run the four containers on hardware with a single tenant: you.

See the machines

Related on Server Room

Other use cases