Can I self-host Zapier?
YES · ONE EVENING— setup effort 2 of 4YES — it's called Activepieces. It takes one prompt, a 4096 MB VPS, and about 105 minutes. That is $29.99 a month you stop paying Zapier — $359.88 a year on the Professional plan, a metered rate, not a whole bill.
Why people pay for Zapier
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
Zapier sells the integrations, not the automation engine. Someone else has already read eight thousand APIs, kept up with every one of their breaking changes, and holds the OAuth client registrations that let you connect an account by clicking a button instead of filling in a developer form. The if-this-then-that part is the easy half; the half you are renting is a maintenance contract on other people's APIs, plus a queue that keeps running while you sleep.
| Plan | List price | What it buys |
|---|---|---|
| Free | free | 100 tasks a month, one seat, two-step Zaps only. |
| Professionalthe plan this page prices against | $29.99/mo metered | The entry task tier, 750 tasks a month, billed monthly. $19.99/mo billed annually. The plan price climbs with the task tier you pick, so this is a floor rather than a price. |
| Team | $103.50/mo metered | The entry task tier, 2,000 tasks a month, billed monthly, 25 users included. $69/mo billed annually. Same task-tier ladder as Professional. |
| Enterprise | quote only | Quote only, unlimited users, advanced admin. |
Vendor list prices in USD, read from the pricing page on 2026-08-05 · confidence: medium
Replaced by Activepieces
One project, named before the prompt, so you know what you are about to install.
Trigger-and-action automation with a visual builder, run on your own box, with no task meter.
The closest thing to a like-for-like swap that you can actually run on one small server. It keeps the shape people are paying for, a visual builder with triggers, actions and branching plus a large catalogue of prebuilt pieces, and it drops the task meter entirely: the community edition is uncapped on runs, users and flows. What you take on is the half Zapier was really selling. Your own OAuth registration for each service you connect, your own encryption key standing between a backup and every token in it, and flow code that runs unsandboxed with your container's network reach. Three containers, one evening, and the bill stops.
The swap
You'd run
Activepieces
ONE EVENING · ~105 min to running · 4096 MB RAM
Zapier Professional · a metered rate, not a whole bill · vendor list price · checked 2026-08-05 · source · confidence: medium
Before you start
- RAM floor
- 4096 MBfloor from upstream docs — not measured by us yet
- Disk
- 20 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~105 min1–3 hours, through the first backup
The prompt
Two paths to the same Activepieces: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
329 lines · 14,571 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install Activepieces 0.86.3-hotfix.1 on that server, reachable at https://<DOMAIN>, behind the
existing Caddy with automatic TLS.
## 1. Preflight
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Say this when you ask: `<DOMAIN>` becomes `AP_FRONTEND_URL`, and every webhook URL this
instance hands out is built from it, so moving it later breaks flows already running. Its A
record must already point at this server.
Activepieces needs 4096 MB of RAM available and 20 GB free on /srv. All three images publish
amd64 and arm64. Measure all four first:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
If available RAM is under 4096 MB or free disk is under 20 GB, print both numbers and stop. Do
not install and hope. Upstream sizes a combined API-and-worker container at roughly 1 GB per
concurrent flow on top of a 2 GB API tier. If `dig +short` prints nothing, print that and stop.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/activepieces /srv/activepieces/backups /srv/activepieces/cache
sudo install -d -m 700 /srv/activepieces/postgres /srv/activepieces/redis
ls -la /srv/activepieces
```
Assert: `ls -la` shows `backups` and `cache` owned by the login user, and `postgres` and
`redis` at mode `700` owned by root. Leave those two alone: both images chown their own data
directory at first start, and one already chowned to yourself makes PostgreSQL refuse to
initialise. `cache` holds downloaded piece packages, which is a cache and not user data.
## 3. Secrets
Three secrets: the encryption key that protects stored connection credentials, the JWT signing
secret, and the PostgreSQL password. Generate all three on the server. Do not print any of
them, do not repeat them in your summary, and do not put them in a log line. Upstream documents
the encryption key as 32 hexadecimal characters, which is `-hex 16`; that length is not
optional.
```bash
umask 077
cat > /srv/activepieces/.env <<EOF
AP_FRONTEND_URL=https://<DOMAIN>
AP_ENCRYPTION_KEY=$(openssl rand -hex 16)
AP_JWT_SECRET=$(openssl rand -hex 32)
AP_POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 /srv/activepieces/.env
umask 022
ls -l /srv/activepieces/.env
```
Assert: the file exists with mode `-rw-------`. Tell the user what the encryption key is for:
every credential they hand a piece is encrypted with it and unreadable without it. It belongs
in their password manager tonight, read with
`sudo grep AP_ENCRYPTION_KEY /srv/activepieces/.env`.
## 4. compose.yml
```bash
cat > /srv/activepieces/compose.yml <<'EOF'
# Activepieces · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# compose install ..... https://www.activepieces.com/docs/install/options/docker-compose
# variable reference .. https://www.activepieces.com/docs/install/reference/environment-variables
# sizing and sandbox .. https://www.activepieces.com/docs/install/configure-operate/production-setup
#
# Three services: Activepieces, the PostgreSQL that holds the flows and the run
# history, and the Redis that holds the job queue. Upstream's own compose file
# splits the API and five worker replicas apart; this one does not, because
# AP_CONTAINER_TYPE defaults to WORKER_AND_APP and one image runs both roles.
# PostgreSQL is the pgvector image because the knowledge base asks the database
# for that extension at every boot and drops the feature when it is absent.
# Upstream's compose pins pgvector 0.8.0-pg14; this file runs the pg16 line,
# the major upstream's own CI runs its Postgres suite against.
#
# Neither the database nor the queue declares `ports:`, and 8095 binds to
# loopback, so the host's Caddy is the only thing that reaches this stack.
# AP_EXECUTION_MODE is upstream's choice for a single-tenant install and the
# image default: flow code runs with this container's own reach.
# AP_WORKER_CONCURRENCY is 2, roughly 1 GB per concurrent flow on top of the API
# tier, and the queue dashboard stays off because it stops the boot when it is on
# with no credentials set. Digests read 2026-08-05; all three images publish
# amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
postgres:
image: pgvector/pgvector:0.8.6-pg16@sha256:a36250871de0833b8757561c72f2477ef1ddd1101afa4e617fb552e0de514c6b
container_name: activepieces-db
restart: unless-stopped
environment:
POSTGRES_DB: activepieces
POSTGRES_USER: activepieces
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
volumes:
- /srv/activepieces/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
interval: 10s
retries: 12
redis:
image: redis:7.4.10-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
container_name: activepieces-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- /srv/activepieces/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 12
activepieces:
image: ghcr.io/activepieces/activepieces:0.86.3-hotfix.1@sha256:4da6910cf46dbc38857c8c4fac6ba867ab804b8a3a8551d672d4490cb1245566
container_name: activepieces
restart: unless-stopped
env_file: /srv/activepieces/.env
environment:
AP_ENVIRONMENT: prod
AP_CONTAINER_TYPE: WORKER_AND_APP
AP_DB_TYPE: POSTGRES
AP_POSTGRES_HOST: postgres
AP_POSTGRES_PORT: "5432"
AP_POSTGRES_DATABASE: activepieces
AP_POSTGRES_USERNAME: activepieces
AP_REDIS_TYPE: STANDALONE
AP_REDIS_HOST: redis
AP_REDIS_PORT: "6379"
AP_EXECUTION_MODE: UNSANDBOXED
AP_WORKER_CONCURRENCY: "2"
AP_QUEUE_UI_ENABLED: "false"
AP_TELEMETRY_ENABLED: "false"
volumes:
- /srv/activepieces/cache:/usr/src/app/cache
ports:
- "127.0.0.1:8095:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
EOF
cd /srv/activepieces && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. Three services, one published port, and neither the database
nor the queue publishes anything. `${AP_POSTGRES_PASSWORD}` comes from the `.env` step 3 wrote,
which compose reads from the project directory.
## 5. Caddy and TLS
Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-activepieces
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Activepieces · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.activepieces.com/docs/install/configure-operate/setup-ssl and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. That hostname is also
# AP_FRONTEND_URL in .env, and every webhook URL this instance hands out is built
# from it, so it is the value here you cannot change once flows are running.
<DOMAIN> {
# The flow editor holds a websocket open for live run output. Upstream's proxy
# example passes the upgrade headers by hand; Caddy does it without being told.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
# Not no-referrer: connecting a piece sends the user out to a third-party
# OAuth consent screen and back, and some providers check the origin.
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
encode zstd gzip
# 8095 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8095
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-activepieces, reload, and report what it objected to. Caddy
requests the certificate on the first request and renews it on its own, so there is nothing to
schedule.
## 6. Firewall
Two ports open, both Caddy's. Idempotent, so on a Prompt Zero box they change nothing:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, 443/udp
is HTTP/3. 8095 stays closed because it is bound to 127.0.0.1, and 5432 and 6379 stay closed
because compose never publishes them at all. Assert: `ufw status verbose` prints
`Status: active`, shows 80, 443/tcp and 443/udp, and no rule for 8095, 5432 or 6379.
## 7. Start and verify
Activepieces runs its own migrations on the way up, then syncs the piece catalogue metadata, so
the first boot is slow. The image's health check waits 60 seconds before it asks.
```bash
cd /srv/activepieces
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/v1/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS https://<DOMAIN>/api/v1/health
curl -sS https://<DOMAIN>/api/v1/flags | grep -o '"USER_CREATED":[a-z]*' || echo "USER_CREATED absent"
```
Assert, all three, and print what you received for each. The loop ends on `200`. The health
response is exactly `{"status":"Healthy"}`. The flags call prints `USER_CREATED absent`, which
is how a fresh instance says nobody has registered yet. If any of the three misses, stop, run
`docker compose logs --tail 40 activepieces` and `docker compose logs --tail 20 postgres`, and
name the likely cause: a `502` past ten minutes points at step 4, a database that never reports
healthy points at step 2, a certificate error at step 5. A running container is not success.
The first screen is at https://<DOMAIN>/sign-up and shows the heading `Create a new account`
over a form asking for a first name, a last name, an email and a password.
STOP: tell the user to open https://<DOMAIN>/sign-up, create the first account, and wait. Do
not continue until they confirm. That account owns the instance. Then confirm registration has
closed behind them:
```bash
curl -sS https://<DOMAIN>/api/v1/flags | grep -o '"USER_CREATED":true'
```
Assert: that prints `"USER_CREATED":true`. From here a second sign-up is answered against the
platform the first account created, and that path requires an invitation unless
`AP_ALLOW_OPEN_SIGN_UP` is set, which this install never sets. Both asserts must pass.
## 8. First backup and restore
Two artifacts. The database holds the flows, the connections and the run history. The config
archive holds the files that rebuild the service around them, encryption key included.
```bash
cd /srv/activepieces
docker compose exec -T postgres pg_dump -U activepieces -d activepieces | gzip > /srv/activepieces/backups/activepieces-db-$(date +%F).sql.gz
sudo tar -C /srv/activepieces -czf /srv/activepieces/backups/activepieces-config-$(date +%F).tar.gz compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/activepieces/backups/
```
Assert: both files exist and both are non-empty. Print both sizes. Nothing is stopped:
`pg_dump` snapshots a running database consistently. Redis is not backed up and does not need
to be: it carries jobs in flight, not the record of what the flows are.
A backup on the same disk is not a backup, so run this from the user's machine:
```bash
mkdir -p ~/backups/activepieces
scp vps:/srv/activepieces/backups/* ~/backups/activepieces/
```
To restore: `docker compose down`, `sudo rm -rf /srv/activepieces/postgres`, recreate it as in
step 2, untar the config archive into /srv/activepieces so `.env` is back before anything
starts, `docker compose up -d postgres`, wait for it to report healthy, pipe `gunzip -c` on the
`.sql.gz` into `docker compose exec -T postgres psql -U activepieces -d activepieces`, then
`docker compose up -d`. Tell the user the stake: a database restored without the matching
`AP_ENCRYPTION_KEY` comes back with every flow intact and every credential unreadable, so the
two artifacts travel together or neither is worth keeping.
## 9. Updating later
New versions are listed at https://github.com/activepieces/activepieces/releases. Take both
backups first, then edit the image line in /srv/activepieces/compose.yml to the new tag and
digest:
```bash
cd /srv/activepieces
docker compose pull
docker compose up -d
docker compose logs --tail 30 activepieces
```
Activepieces migrates its own database on the way up, so watch that log until it settles, then
re-run step 7's health check before calling the update done.
## 10. What will probably go wrong
The first boot looks like a failed install for several minutes. I watched Caddy answer `502`
over and over while the container ran migrations and pulled the piece catalogue metadata, and
the pull to start editing the compose file was strong. Nothing was wrong. The image's health
check does not begin probing for 60 seconds, and step 7's loop waits ten minutes for a reason.
Before touching anything, run `docker compose logs --tail 40 activepieces`: a log still moving
is an install still working.
## 11. Out of scope
- Do not configure SMTP. No `AP_SMTP_` variable is set here, and the community edition verifies
the first account itself rather than mailing a link.
- Do not set `AP_GOOGLE_CLIENT_ID`, `AP_GOOGLE_CLIENT_SECRET` or any SSO variable. First login
is an email address and a password, and single sign-on is a paid-edition feature.
- Do not split the worker into its own container, raise `AP_WORKER_CONCURRENCY`, or configure
S3 storage. Those are the production shape for a fleet, and this is one machine.
- Do not set `AP_NETWORK_MODE=STRICT` or change `AP_EXECUTION_MODE`. Upstream states that the
strict guard is best-effort inside the process, not a boundary against hostile code.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.
You are installing Activepieces 0.86.3-hotfix.1 on a VPS where Prompt Zero is done: `ssh vps`
works, Docker and Caddy are installed, the firewall is default-deny. Run everything over
`ssh vps` unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record
already points at the box.
Read this before step 1. `<DOMAIN>` becomes `AP_FRONTEND_URL`, and every webhook URL and OAuth
redirect this instance hands out is built from it. Flows that are already running keep pointing
at the old name if you move it, so pick the hostname you intend to keep.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
You should see: at least `4096` MB available, at least `20` G free, `amd64` or `arm64`, and
your server's IP on the last line.
If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a hostname that does not
resolve, and failed attempts count against a rate limit you cannot see. On memory, the floor is
real rather than cautious: upstream sizes a container running the API and a worker together at
roughly 1 GB per concurrent flow on top of a 2 GB API tier, and this install runs two flows at
once. A 2 GB box will start, then be killed by the kernel partway through your first real flow.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/activepieces /srv/activepieces/backups /srv/activepieces/cache
sudo install -d -m 700 /srv/activepieces/postgres /srv/activepieces/redis
ls -la /srv/activepieces
```
You should see: `backups` and `cache` owned by you, and `postgres` and `redis` at mode
`drwx------` owned by root.
If you do not: leave those last two owned by root on purpose. Both images chown their own data
directory the first time they start, and a PostgreSQL directory you have already chowned to
yourself makes the container refuse to initialise. `cache` holds the piece packages the app
downloads; it is a cache, and losing it costs a slow boot and nothing else.
## 3. Secrets
Three secrets: the encryption key that protects every credential you will hand a piece, the JWT
signing secret, and the PostgreSQL password. All three are generated here, on the server, into
a file only you can read. The encryption key is 32 hexadecimal characters because upstream says
so, which is why it is `-hex 16` while the other two are `-hex 32`.
```bash
umask 077
cat > /srv/activepieces/.env <<EOF
AP_FRONTEND_URL=https://<DOMAIN>
AP_ENCRYPTION_KEY=$(openssl rand -hex 16)
AP_JWT_SECRET=$(openssl rand -hex 32)
AP_POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 /srv/activepieces/.env
umask 022
ls -l /srv/activepieces/.env
```
You should see: mode `-rw-------`, your own username twice, and the path. Replace `<DOMAIN>` on
the first line with your real hostname before you paste.
Do not paste that file, any of the three values, or any command output containing them into
this chat window. The agent path never sees them; this one will hand them to a third party
unless you are deliberate about it.
If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens if
you pasted the lines separately in different shells. Run `chmod 600 /srv/activepieces/.env` and
carry on. If the file already existed from an earlier attempt, this block has now overwritten
all three secrets, which is fine before the database exists and a problem afterwards:
PostgreSQL keeps the password it was created with, so a changed `AP_POSTGRES_PASSWORD` on an
existing data directory shows up as an authentication failure in the activepieces log rather
than anything about passwords. Read the encryption key once with
`sudo grep AP_ENCRYPTION_KEY /srv/activepieces/.env` and put it in your password manager: every
connection you create is encrypted with it, and a restored database without it is a list of
flows you cannot run.
## 4. compose.yml
Paste the whole block at once, including the last two lines.
```bash
cat > /srv/activepieces/compose.yml <<'EOF'
# Activepieces · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# compose install ..... https://www.activepieces.com/docs/install/options/docker-compose
# variable reference .. https://www.activepieces.com/docs/install/reference/environment-variables
# sizing and sandbox .. https://www.activepieces.com/docs/install/configure-operate/production-setup
#
# Three services: Activepieces, the PostgreSQL that holds the flows and the run
# history, and the Redis that holds the job queue. Upstream's own compose file
# splits the API and five worker replicas apart; this one does not, because
# AP_CONTAINER_TYPE defaults to WORKER_AND_APP and one image runs both roles.
# PostgreSQL is the pgvector image because the knowledge base asks the database
# for that extension at every boot and drops the feature when it is absent.
# Upstream's compose pins pgvector 0.8.0-pg14; this file runs the pg16 line,
# the major upstream's own CI runs its Postgres suite against.
#
# Neither the database nor the queue declares `ports:`, and 8095 binds to
# loopback, so the host's Caddy is the only thing that reaches this stack.
# AP_EXECUTION_MODE is upstream's choice for a single-tenant install and the
# image default: flow code runs with this container's own reach.
# AP_WORKER_CONCURRENCY is 2, roughly 1 GB per concurrent flow on top of the API
# tier, and the queue dashboard stays off because it stops the boot when it is on
# with no credentials set. Digests read 2026-08-05; all three images publish
# amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
postgres:
image: pgvector/pgvector:0.8.6-pg16@sha256:a36250871de0833b8757561c72f2477ef1ddd1101afa4e617fb552e0de514c6b
container_name: activepieces-db
restart: unless-stopped
environment:
POSTGRES_DB: activepieces
POSTGRES_USER: activepieces
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
volumes:
- /srv/activepieces/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
interval: 10s
retries: 12
redis:
image: redis:7.4.10-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
container_name: activepieces-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- /srv/activepieces/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 12
activepieces:
image: ghcr.io/activepieces/activepieces:0.86.3-hotfix.1@sha256:4da6910cf46dbc38857c8c4fac6ba867ab804b8a3a8551d672d4490cb1245566
container_name: activepieces
restart: unless-stopped
env_file: /srv/activepieces/.env
environment:
AP_ENVIRONMENT: prod
AP_CONTAINER_TYPE: WORKER_AND_APP
AP_DB_TYPE: POSTGRES
AP_POSTGRES_HOST: postgres
AP_POSTGRES_PORT: "5432"
AP_POSTGRES_DATABASE: activepieces
AP_POSTGRES_USERNAME: activepieces
AP_REDIS_TYPE: STANDALONE
AP_REDIS_HOST: redis
AP_REDIS_PORT: "6379"
AP_EXECUTION_MODE: UNSANDBOXED
AP_WORKER_CONCURRENCY: "2"
AP_QUEUE_UI_ENABLED: "false"
AP_TELEMETRY_ENABLED: "false"
volumes:
- /srv/activepieces/cache:/usr/src/app/cache
ports:
- "127.0.0.1:8095:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
EOF
cd /srv/activepieces && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK` and nothing else.
If you do not: `env file /srv/activepieces/.env not found` means step 3 did not write the file.
`services must be a mapping` means the indentation was lost between the page and your terminal:
run `rm /srv/activepieces/compose.yml` and paste again in one go. The `${AP_POSTGRES_PASSWORD}`
on the postgres service is not a typo; compose reads it out of the `.env` in the same
directory, which is why that file has to exist before this command runs.
## 5. Caddy and TLS
This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in
the block with your hostname before you paste. The first line takes a copy, because a syntax
error here takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-activepieces
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Activepieces · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.activepieces.com/docs/install/configure-operate/setup-ssl and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. That hostname is also
# AP_FRONTEND_URL in .env, and every webhook URL this instance hands out is built
# from it, so it is the value here you cannot change once flows are running.
<DOMAIN> {
# The flow editor holds a websocket open for live run output. Upstream's proxy
# example passes the upgrade headers by hand; Caddy does it without being told.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
# Not no-referrer: connecting a piece sends the user out to a third-party
# OAuth consent screen and back, and some providers check the origin.
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
encode zstd gzip
# 8095 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8095
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
You should see: `Valid configuration` from validate, and no output at all from reload.
If you do not: run `sudo cp /etc/caddy/Caddyfile.before-activepieces /etc/caddy/Caddyfile`,
reload, and paste again. The flow editor streams live run output over a websocket, and
upstream's own proxy example sets the upgrade headers by hand. Caddy does that without being
told, so there is no upgrade stanza in the block and nothing missing from it.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8095`, `5432` or `6379`.
If you do not: delete anything for those three with `sudo ufw delete allow 8095`. 8095 is bound
to 127.0.0.1 by the compose file, and 5432 and 6379 are never published at all, so neither the
database nor the queue has a host port a firewall rule could apply to. 80/tcp redirects to
HTTPS and answers the ACME challenge, 443/tcp is the only way in, and 443/udp is HTTP/3, which
Caddy offers by default. `Status: inactive` is a different problem: Prompt Zero left this
firewall enabled, so something has turned it off since, and `sudo ufw enable` puts it back
before you go any further.
## 7. Start and verify
Activepieces runs its own database migrations on the way up and then syncs the metadata for the
piece catalogue. The first boot is slow and the loop below is built to wait for it.
```bash
cd /srv/activepieces
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/v1/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS https://<DOMAIN>/api/v1/health
curl -sS https://<DOMAIN>/api/v1/flags | grep -o '"USER_CREATED":[a-z]*' || echo "USER_CREATED absent"
```
You should see, in order: the loop climbing through `502` and then reaching `200`, then
`{"status":"Healthy"}`, then `USER_CREATED absent`.
If you do not: the `502` while it climbs is normal, because Caddy has a site block for a
container that is still migrating. The image's own health check does not begin probing for 60
seconds. If the loop never reaches `200`, run `docker compose logs --tail 20 postgres` first,
because a database that never reports healthy is step 2 done wrong, and
`docker compose logs --tail 40 activepieces` second. A certificate error rather than a `502`
points at step 5 or at DNS.
Now open https://<DOMAIN>/sign-up in a browser. The first screen shows the heading
`Create a new account` over a form asking for a first name, a last name, an email address and a
password. Create the account. It owns this instance.
Then confirm registration has closed behind you:
```bash
curl -sS https://<DOMAIN>/api/v1/flags | grep -o '"USER_CREATED":true'
```
You should see: `"USER_CREATED":true`.
If you do not: an empty result means the sign-up did not complete, so reload the page and check
whether you are logged in. Once that flag is true, a second sign-up is answered against the
platform your account created, and that path requires an invitation unless
`AP_ALLOW_OPEN_SIGN_UP` is set, which this install never sets. Both of those checks passing is
what success means here. A running container is not success.
## 8. First backup and restore
Two artifacts. The database holds the flows, the connections and the run history. The config
archive holds the files that rebuild the service around them, including the encryption key.
```bash
cd /srv/activepieces
docker compose exec -T postgres pg_dump -U activepieces -d activepieces | gzip > /srv/activepieces/backups/activepieces-db-$(date +%F).sql.gz
sudo tar -C /srv/activepieces -czf /srv/activepieces/backups/activepieces-config-$(date +%F).tar.gz compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/activepieces/backups/
```
You should see: two files, both a few kilobytes on a fresh install. Nothing goes offline:
`pg_dump` snapshots a running database consistently. Redis is not in the backup and does not
need to be, because it carries jobs in flight rather than the record of what the flows are.
If you do not: a `.sql.gz` of about 20 bytes is an empty dump, which means `pg_dump` failed and
the shell created the file anyway. Run the dump line without `| gzip` to read the error.
A backup on the same disk as the data is not a backup. Run this one on your own machine, not
the server:
```bash
mkdir -p ~/backups/activepieces
scp vps:/srv/activepieces/backups/* ~/backups/activepieces/
```
You should see: two files copied, and both listed by `ls -lh ~/backups/activepieces/`.
If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:`
prefix only means something on your own machine, where the `vps` alias Prompt Zero created
lives.
Now prove the restore, today, while the only thing at risk is an empty instance:
```bash
cd /srv/activepieces
docker compose down
sudo rm -rf /srv/activepieces/postgres
sudo install -d -m 700 /srv/activepieces/postgres
docker compose up -d postgres
sleep 30
gunzip -c /srv/activepieces/backups/activepieces-db-$(date +%F).sql.gz | docker compose exec -T postgres psql -U activepieces -d activepieces
docker compose up -d
sleep 60
curl -sS https://<DOMAIN>/api/v1/health
```
You should see: `CREATE TABLE` and `COPY` lines from psql, then `{"status":"Healthy"}`.
If you do not: `role "activepieces" does not exist` means the database container had not
finished initialising, so wait longer and run the `gunzip` line again. Understand the stake
before you skip this step. The dump and the config archive are one backup in two files:
restored without the matching `AP_ENCRYPTION_KEY` from `.env`, that database comes back with
every flow intact and every stored credential unreadable, and there is no recovery from that
other than reconnecting every service by hand.
## 9. Updating later
New versions are listed at https://github.com/activepieces/activepieces/releases. Take both
backup artifacts first, then edit the image line in /srv/activepieces/compose.yml to the new
tag and its digest.
```bash
cd /srv/activepieces
docker compose pull
docker compose up -d
docker compose logs --tail 30 activepieces
```
You should see: migration output, then the server starting, and no repeating restart.
If you do not: put the old tag and digest back and run the same three commands. Then re-run the
health check from step 7 before you call the update done, and open one flow in the editor as
well, because a service that answers `Healthy` can still be failing to run flows if a migration
stopped halfway.
## 10. What will probably go wrong
The first boot looks like a failed install for several minutes. I watched Caddy answer `502`
over and over while the container ran migrations and pulled the piece catalogue metadata, and
the pull to start editing the compose file was strong. Nothing was wrong. The image's health
check does not begin probing for 60 seconds, and the loop in step 7 waits ten minutes for a
reason. Before touching anything, run `docker compose logs --tail 40 activepieces`: a log still
moving is an install still working.
## 11. Out of scope
- Do not configure SMTP. No `AP_SMTP_` variable is set here, and the community edition verifies
the first account itself rather than mailing a link.
- Do not set `AP_GOOGLE_CLIENT_ID`, `AP_GOOGLE_CLIENT_SECRET` or any SSO variable. First login
is an email address and a password, and single sign-on is a paid-edition feature.
- Do not split the worker into its own container, raise `AP_WORKER_CONCURRENCY`, or configure
S3 storage. Those are the production shape for a fleet, and this is one machine.
- Do not set `AP_NETWORK_MODE=STRICT` or change `AP_EXECUTION_MODE`. Upstream states that the
strict guard is best-effort inside the process, not a boundary against hostile code.333 lines · 14,970 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install Activepieces 0.86.3-hotfix.1, with its PostgreSQL and Redis, under
~/selfhost/activepieces, answering at http://localhost:8095.
## 1. Preflight
Say this before step 2 runs; it decides whether the user wants this install. Schedules and
polling triggers work, while the machine is awake. Webhook triggers do not: the URL handed to a
third party starts http://localhost:8095, which nothing outside this computer can deliver to.
Detect the OS and measure:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. This install needs 4096 MB of RAM
available and 20 GB free on the home disk; all three images publish amd64 and arm64. On macOS
and Windows that figure is the host's, out of which Docker Desktop takes its share. Under
either floor, print both numbers and stop.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/activepieces/backups ~/selfhost/activepieces/cache
ls -la ~/selfhost/activepieces
```
Assert: `ls -la` shows `backups` and `cache`, both owned by the user. There is no `data`
folder: flows and queue live in volumes Docker manages.
## 4. Secrets
Three secrets: the encryption key protecting stored connection credentials, the JWT signing
secret, and the PostgreSQL password. Generate all three here, print none, and keep them out of
your summary and any log line. Upstream documents the encryption key as 32 hex characters,
`-hex 16`; that length is not optional.
```bash
umask 077
cat > ~/selfhost/activepieces/.env <<EOF
AP_FRONTEND_URL=http://localhost:8095
AP_ENCRYPTION_KEY=$(openssl rand -hex 16)
AP_JWT_SECRET=$(openssl rand -hex 32)
AP_POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 ~/selfhost/activepieces/.env
umask 022
ls -l ~/selfhost/activepieces/.env
```
Assert: the file exists with mode `-rw-------`; Git Bash ships openssl, so this runs the same
everywhere. Every credential the user hands a piece is encrypted with that key, so tell them to
put it in their password manager tonight, read with
`grep AP_ENCRYPTION_KEY ~/selfhost/activepieces/.env`. On Windows those mode bits are advisory
and the real boundary is the user's own account.
## 5. compose.yml
```bash
cat > ~/selfhost/activepieces/compose.yml <<'EOF'
# Activepieces · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# compose install ..... https://www.activepieces.com/docs/install/options/docker-compose
# variable reference .. https://www.activepieces.com/docs/install/reference/environment-variables
# sizing and sandbox .. https://www.activepieces.com/docs/install/configure-operate/production-setup
#
# Three services, every path relative to ~/selfhost/activepieces/ so one file
# works on macOS, Linux and Windows. One image runs the API and the worker;
# PostgreSQL is the pgvector image because the knowledge base asks for that
# extension at boot. AP_EXECUTION_MODE is upstream's choice for single-tenant and
# the image default: flow code runs with this container's reach, home network
# included. 8095 binds to loopback and AP_FRONTEND_URL is http://localhost:8095,
# this computer only. Digests read 2026-08-05; all three publish amd64 and arm64.
#
# Two named volumes rather than bind mounts: PostgreSQL chowns its data directory
# to its own uid and Redis chowns /data to the redis user, and Docker Desktop's
# Windows file sharing grants neither on a home-directory bind mount. The piece
# cache stays a relative bind, visible in Finder or Explorer.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
postgres:
image: pgvector/pgvector:0.8.6-pg16@sha256:a36250871de0833b8757561c72f2477ef1ddd1101afa4e617fb552e0de514c6b
container_name: activepieces-db
restart: unless-stopped
environment:
POSTGRES_DB: activepieces
POSTGRES_USER: activepieces
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
volumes:
- activepieces-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
interval: 10s
retries: 12
redis:
image: redis:7.4.10-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
container_name: activepieces-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- activepieces-redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 12
activepieces:
image: ghcr.io/activepieces/activepieces:0.86.3-hotfix.1@sha256:4da6910cf46dbc38857c8c4fac6ba867ab804b8a3a8551d672d4490cb1245566
container_name: activepieces
restart: unless-stopped
env_file: ./.env
environment:
AP_ENVIRONMENT: prod
AP_CONTAINER_TYPE: WORKER_AND_APP
AP_DB_TYPE: POSTGRES
AP_POSTGRES_HOST: postgres
AP_POSTGRES_PORT: "5432"
AP_POSTGRES_DATABASE: activepieces
AP_POSTGRES_USERNAME: activepieces
AP_REDIS_TYPE: STANDALONE
AP_REDIS_HOST: redis
AP_REDIS_PORT: "6379"
AP_EXECUTION_MODE: UNSANDBOXED
AP_WORKER_CONCURRENCY: "1"
AP_QUEUE_UI_ENABLED: "false"
AP_TELEMETRY_ENABLED: "false"
volumes:
- ./cache:/usr/src/app/cache
ports:
- "127.0.0.1:8095:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
activepieces-pgdata:
activepieces-redisdata:
EOF
cd ~/selfhost/activepieces && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`.
## 6. Nothing is public
Three absences, each a decision:
- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
http://localhost as a secure context anyway, so pages needing crypto still work.
- No firewall rule. Nothing is published beyond loopback, so no port needs closing.
8095 is bound to 127.0.0.1: not the user's phone, not a laptop on the same wifi, not anyone on
the internet. That is the point of this path, and why a webhook has nowhere to arrive.
```bash
grep -n '127.0.0.1' ~/selfhost/activepieces/compose.yml
```
Assert: one line, `- "127.0.0.1:8095:80"`.
## 7. Start and verify
Activepieces runs its migrations, then syncs the piece catalogue metadata, so first boot is
slow. The image's health check waits 60 seconds before it asks.
```bash
cd ~/selfhost/activepieces
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8095/api/v1/health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS http://localhost:8095/api/v1/health
curl -sS http://localhost:8095/api/v1/flags | grep -o '"USER_CREATED":[a-z]*' || echo "USER_CREATED absent"
```
Assert all three, and print what you received for each: the loop ends on `200`; the health
response is exactly `{"status":"Healthy"}`; the flags call prints `USER_CREATED absent`, which
is how a fresh instance says nobody has registered. If any misses, stop, run
`docker compose logs --tail 40 activepieces` and `docker compose logs --tail 20 postgres`, and
name the likely cause: a database that never reports healthy points at step 4. If
`port is already allocated` came back, find what holds 8095 with
`lsof -nP -iTCP:8095 -sTCP:LISTEN` and stop until it is free. A running container is not
success.
The first screen is at http://localhost:8095/sign-up and shows the heading
`Create a new account` over a form asking for a first name, a last name, an email and a
password.
STOP: tell the user to open that URL, create the first account, and wait. Do not continue until
they confirm. That account owns the instance. Then confirm registration closed:
```bash
curl -sS http://localhost:8095/api/v1/flags | grep -o '"USER_CREATED":true'
```
Assert: that prints `"USER_CREATED":true`. A second sign-up is now answered against the
platform the first account created, and that path needs an invitation unless
`AP_ALLOW_OPEN_SIGN_UP` is set, which this install never does. Both asserts must pass.
## 8. First backup and restore
Two artifacts: a database dump with the flows, connections and run history, and a config
archive with the two files that rebuild the service around it, encryption key included.
```bash
cd ~/selfhost/activepieces
docker compose exec -T postgres pg_dump -U activepieces -d activepieces | gzip > ~/selfhost/activepieces/backups/activepieces-db-$(date +%F).sql.gz
tar -C ~/selfhost/activepieces -czf ~/selfhost/activepieces/backups/activepieces-config-$(date +%F).tar.gz compose.yml .env
ls -lh ~/selfhost/activepieces/backups/
```
Assert: both files exist and are non-empty. Print both sizes. Nothing is stopped: `pg_dump`
snapshots a running database consistently. Redis is not backed up: it carries jobs in flight,
not the flows.
Both archives sit on the same disk as the data, and on a laptop disk and machine fail together.
Ask for a destination that leaves this computer, a sync folder or a USB stick, and copy both
there with `cp`; in Git Bash a Windows drive is `/d/Backups`. Assert: the user confirms both
filenames are listed there. If they have neither, say plainly that this install has no backup.
To restore. `cd ~/selfhost/activepieces` and untar the config archive there first, so
compose.yml and .env are back before any container starts: PostgreSQL reads
`AP_POSTGRES_PASSWORD` from .env the moment it initialises an empty volume. Then
`docker compose down -v`, the one place `-v` belongs, `docker compose up -d postgres`, wait 30
seconds for healthy, pipe `gunzip -c` on the `.sql.gz` into
`docker compose exec -T postgres psql -U activepieces -d activepieces`, then
`docker compose up -d` and re-run step 7's health check. Restored without the matching
`AP_ENCRYPTION_KEY`, that database comes back with every flow intact and nothing able to run.
## 9. Updating later
New versions are listed at https://github.com/activepieces/activepieces/releases. Back up
first, then edit the image line in compose.yml to the new tag and digest:
```bash
cd ~/selfhost/activepieces
docker compose pull
docker compose up -d
docker compose logs --tail 30 activepieces
```
Watch that log until it settles, then re-run the health check.
## 10. What will probably go wrong
I closed the lid on a Friday and came back Monday to a flow that had not run since. Nothing
crashed: a scheduled trigger fires only while this computer is awake and Docker is up, and a
sleeping laptop is neither. `restart: unless-stopped` acts only once the daemon is running, so
a reboot without Docker Desktop's start-at-login setting leaves nothing on 8095. Turn that
setting on, and after a reboot run `cd ~/selfhost/activepieces && docker compose up -d` before
concluding anything is wrong.
## 11. Out of scope
- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not install a tunnel to make webhook triggers reachable, and do not rebind 8095 to
0.0.0.0. Either puts a machine holding every credential the user connected onto a network.
- Do not configure SMTP, `AP_GOOGLE_CLIENT_ID`, or any SSO variable. First login is an email
address and a password; SSO is a paid-edition feature.
- Do not change `AP_EXECUTION_MODE` or `AP_NETWORK_MODE`. Upstream states the strict guard is
best-effort in-process, not a boundary against hostile code.compose.local.ymlthe services, pinned · local layout82 lines
# Activepieces · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# compose install ..... https://www.activepieces.com/docs/install/options/docker-compose
# variable reference .. https://www.activepieces.com/docs/install/reference/environment-variables
# sizing and sandbox .. https://www.activepieces.com/docs/install/configure-operate/production-setup
#
# Three services, every path relative to ~/selfhost/activepieces/ so one file
# works on macOS, Linux and Windows. One image runs the API and the worker;
# PostgreSQL is the pgvector image because the knowledge base asks for that
# extension at boot. AP_EXECUTION_MODE is upstream's choice for single-tenant and
# the image default: flow code runs with this container's reach, home network
# included. 8095 binds to loopback and AP_FRONTEND_URL is http://localhost:8095,
# this computer only. Digests read 2026-08-05; all three publish amd64 and arm64.
#
# Two named volumes rather than bind mounts: PostgreSQL chowns its data directory
# to its own uid and Redis chowns /data to the redis user, and Docker Desktop's
# Windows file sharing grants neither on a home-directory bind mount. The piece
# cache stays a relative bind, visible in Finder or Explorer.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
postgres:
image: pgvector/pgvector:0.8.6-pg16@sha256:a36250871de0833b8757561c72f2477ef1ddd1101afa4e617fb552e0de514c6b
container_name: activepieces-db
restart: unless-stopped
environment:
POSTGRES_DB: activepieces
POSTGRES_USER: activepieces
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
volumes:
- activepieces-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
interval: 10s
retries: 12
redis:
image: redis:7.4.10-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
container_name: activepieces-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- activepieces-redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 12
activepieces:
image: ghcr.io/activepieces/activepieces:0.86.3-hotfix.1@sha256:4da6910cf46dbc38857c8c4fac6ba867ab804b8a3a8551d672d4490cb1245566
container_name: activepieces
restart: unless-stopped
env_file: ./.env
environment:
AP_ENVIRONMENT: prod
AP_CONTAINER_TYPE: WORKER_AND_APP
AP_DB_TYPE: POSTGRES
AP_POSTGRES_HOST: postgres
AP_POSTGRES_PORT: "5432"
AP_POSTGRES_DATABASE: activepieces
AP_POSTGRES_USERNAME: activepieces
AP_REDIS_TYPE: STANDALONE
AP_REDIS_HOST: redis
AP_REDIS_PORT: "6379"
AP_EXECUTION_MODE: UNSANDBOXED
AP_WORKER_CONCURRENCY: "1"
AP_QUEUE_UI_ENABLED: "false"
AP_TELEMETRY_ENABLED: "false"
volumes:
- ./cache:/usr/src/app/cache
ports:
- "127.0.0.1:8095:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
activepieces-pgdata:
activepieces-redisdata:agent-readable mirror: /self-host/zapier.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned83 lines
# Activepieces · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# compose install ..... https://www.activepieces.com/docs/install/options/docker-compose
# variable reference .. https://www.activepieces.com/docs/install/reference/environment-variables
# sizing and sandbox .. https://www.activepieces.com/docs/install/configure-operate/production-setup
#
# Three services: Activepieces, the PostgreSQL that holds the flows and the run
# history, and the Redis that holds the job queue. Upstream's own compose file
# splits the API and five worker replicas apart; this one does not, because
# AP_CONTAINER_TYPE defaults to WORKER_AND_APP and one image runs both roles.
# PostgreSQL is the pgvector image because the knowledge base asks the database
# for that extension at every boot and drops the feature when it is absent.
# Upstream's compose pins pgvector 0.8.0-pg14; this file runs the pg16 line,
# the major upstream's own CI runs its Postgres suite against.
#
# Neither the database nor the queue declares `ports:`, and 8095 binds to
# loopback, so the host's Caddy is the only thing that reaches this stack.
# AP_EXECUTION_MODE is upstream's choice for a single-tenant install and the
# image default: flow code runs with this container's own reach.
# AP_WORKER_CONCURRENCY is 2, roughly 1 GB per concurrent flow on top of the API
# tier, and the queue dashboard stays off because it stops the boot when it is on
# with no credentials set. Digests read 2026-08-05; all three images publish
# amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
postgres:
image: pgvector/pgvector:0.8.6-pg16@sha256:a36250871de0833b8757561c72f2477ef1ddd1101afa4e617fb552e0de514c6b
container_name: activepieces-db
restart: unless-stopped
environment:
POSTGRES_DB: activepieces
POSTGRES_USER: activepieces
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
volumes:
- /srv/activepieces/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
interval: 10s
retries: 12
redis:
image: redis:7.4.10-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
container_name: activepieces-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- /srv/activepieces/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 12
activepieces:
image: ghcr.io/activepieces/activepieces:0.86.3-hotfix.1@sha256:4da6910cf46dbc38857c8c4fac6ba867ab804b8a3a8551d672d4490cb1245566
container_name: activepieces
restart: unless-stopped
env_file: /srv/activepieces/.env
environment:
AP_ENVIRONMENT: prod
AP_CONTAINER_TYPE: WORKER_AND_APP
AP_DB_TYPE: POSTGRES
AP_POSTGRES_HOST: postgres
AP_POSTGRES_PORT: "5432"
AP_POSTGRES_DATABASE: activepieces
AP_POSTGRES_USERNAME: activepieces
AP_REDIS_TYPE: STANDALONE
AP_REDIS_HOST: redis
AP_REDIS_PORT: "6379"
AP_EXECUTION_MODE: UNSANDBOXED
AP_WORKER_CONCURRENCY: "2"
AP_QUEUE_UI_ENABLED: "false"
AP_TELEMETRY_ENABLED: "false"
volumes:
- /srv/activepieces/cache:/usr/src/app/cache
ports:
- "127.0.0.1:8095:80"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthyCaddyfilethe hostname and TLS29 lines
# Activepieces · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.activepieces.com/docs/install/configure-operate/setup-ssl and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. That hostname is also
# AP_FRONTEND_URL in .env, and every webhook URL this instance hands out is built
# from it, so it is the value here you cannot change once flows are running.
<DOMAIN> {
# The flow editor holds a websocket open for live run output. Upstream's proxy
# example passes the upgrade headers by hand; Caddy does it without being told.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
# Not no-referrer: connecting a piece sends the user out to a third-party
# OAuth consent screen and back, and some providers check the origin.
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
encode zstd gzip
# 8095 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8095
}install.shthe same install, no agent151 lines
#!/usr/bin/env bash
# Activepieces · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=flows.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
# https://www.activepieces.com/docs/install/options/docker-compose
# https://www.activepieces.com/docs/install/reference/environment-variables
# https://www.activepieces.com/docs/install/configure-operate/production-setup
# https://www.activepieces.com/docs/install/configure-operate/setup-ssl
#
# Three secrets are generated here, on this machine: the encryption key, the JWT
# signing secret and the PostgreSQL password. All three go into
# /srv/activepieces/.env with mode 600 and none of them is ever printed.
#
# DOMAIN_HOST is also AP_FRONTEND_URL, the hostname every webhook URL and OAuth
# redirect is built from. Choose it once; flows already running keep pointing at
# the old name if you move it.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/activepieces}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. flows.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 4096 ] || die "only ${avail_mb} MB of RAM available; the API and two concurrent flows want 4096 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 20 ] || die "only ${avail_gb} GB free on /srv; this install wants 20 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
# --- 2. Lay the files out ----------------------------------------------------
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups" "$APP_DIR/cache"
sudo install -d -m 700 "$APP_DIR/postgres" "$APP_DIR/redis"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
# --- 3. Generate the three secrets, on the server ----------------------------
#
# The encryption key is 32 hexadecimal characters because upstream documents that
# length, which is why it is -hex 16 while the other two are -hex 32. Read them
# later with
# sudo grep -E 'AP_ENCRYPTION_KEY|AP_JWT_SECRET|AP_POSTGRES_PASSWORD' /srv/activepieces/.env
if [ ! -f "$APP_DIR/.env" ]; then
umask 077
cat > "$APP_DIR/.env" <<-ENVFILE
AP_FRONTEND_URL=https://${DOMAIN_HOST}
AP_ENCRYPTION_KEY=$(openssl rand -hex 16)
AP_JWT_SECRET=$(openssl rand -hex 32)
AP_POSTGRES_PASSWORD=$(openssl rand -hex 32)
ENVFILE
chmod 600 "$APP_DIR/.env"
umask 022
fi
cd "$APP_DIR"
docker compose config >/dev/null
# --- 4. Caddy site block, on the host ----------------------------------------
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-activepieces"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 5. Ports: two open, and 8095, 5432 and 6379 are not among them ----------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8095, 5432 and 6379 stay closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 6. Start it -------------------------------------------------------------
#
# Activepieces runs its own database migrations on the way up and then syncs the
# metadata for the piece catalogue, so the first boot takes minutes and Caddy
# answers 502 for most of them.
docker compose pull
docker compose up -d
echo "==> waiting for https://${DOMAIN_HOST}/api/v1/health (this takes minutes on a first boot)"
for _ in $(seq 1 40); do
code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/api/v1/health" || true)"
[ "$code" = "200" ] && break
sleep 15
done
[ "${code:-}" = "200" ] || die "/api/v1/health answered ${code:-nothing}. Check: docker compose logs --tail 40 activepieces"
curl -sS "https://${DOMAIN_HOST}/api/v1/health" | grep -q '"status":"Healthy"' \
|| die "/api/v1/health answered 200 without status Healthy. Check: docker compose logs --tail 40 activepieces"
# Nobody has registered yet: the flag that records the first sign-up is absent
# from the public flags document until an account exists.
if curl -sS "https://${DOMAIN_HOST}/api/v1/flags" | grep -q '"USER_CREATED":true'; then
die "an account already exists on this instance. Stop and work out whose it is before going further."
fi
# --- 7. The first backup, before day one ends --------------------------------
STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose exec -T postgres pg_dump -U activepieces -d activepieces | gzip > "$APP_DIR/backups/activepieces-db-${STAMP}.sql.gz"
sudo tar -C "$APP_DIR" -czf "$APP_DIR/backups/activepieces-config-${STAMP}.tar.gz" compose.yml .env -C /etc/caddy Caddyfile
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/activepieces-db-${STAMP}.sql.gz" ] || die "the database dump is empty"
cat <<-DONE
Activepieces is answering at https://${DOMAIN_HOST}/api/v1/health
1. Open https://${DOMAIN_HOST}/sign-up and create the first account. The
screen says "Create a new account". That account owns this instance, and
once it exists a second sign-up needs an invitation.
Confirm it took:
curl -sS https://${DOMAIN_HOST}/api/v1/flags | grep -o '"USER_CREATED":true'
2. Your encryption key is in $APP_DIR/.env, mode 600. Read it with
sudo grep AP_ENCRYPTION_KEY $APP_DIR/.env
and put it in your password manager. It was not printed here. Every
credential you hand a piece is encrypted with it, and a database restored
without it comes back unreadable.
3. Flow code runs unsandboxed, with this container's own network reach. The
flows you write are the security boundary; treat a flow from someone else
the way you would treat a shell script from someone else.
4. First backup written to $APP_DIR/backups: a database dump and a config
archive. They are on the same disk as the data, which is not a backup.
Copy them somewhere else tonight, and keep the pair together.
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Zapier.
- The core is MIT, but the repository carries a packages/ee tree under a separate commercial licence, and the feature split is real: agents and chat, projects, API access, SSO, user roles, audit logs, secret managers, branding and Git Sync are what the paid editions sell. The community edition you install here is uncapped on runs, users and flows, and stops short of that list.
- Flow code runs unsandboxed, because that is upstream's own guidance for a single-tenant install and the image default. A flow can reach anything this container can reach, including your provider's metadata endpoint. The flows you write are the security boundary, so treat one copied from a stranger the way you would treat a shell script from a stranger.
- One key stands between you and every credential you connect. AP_ENCRYPTION_KEY encrypts the tokens for every service a flow touches, and a database restored without it comes back with every flow intact and nothing able to run. The database dump and the config archive are one backup in two files.
- No mail means no password reset. This install configures no SMTP, so the documented way back into a locked-out account is editing a bcrypt hash in PostgreSQL by hand. Put the account password in a password manager on day one.
- Webhooks are the part that needs a real hostname. Polling triggers work anywhere, but an app that calls you back needs a URL it can resolve, which is why the cloud path wants DNS and the local path honestly gives you half the tool.
Where this came from
“The engine SSRF guard is best-effort, in-process protection. It is not a hard boundary against deliberately malicious code: worker_threads, child_process, native addons, and process.binding can all sidestep the JavaScript-level monkey-patches.”
- Upstream's own compose file runs the API and five worker replicas as separate containers, but AP_CONTAINER_TYPE defaults to WORKER_AND_APP, so one container can run both roles. source
- AP_ENCRYPTION_KEY is a 32-character hexadecimal key used to encrypt connections, and AP_FRONTEND_URL is the public URL every webhook and redirect URL is built from. source
- Upstream sizes a worker at roughly 0.5 vCPU and 1 GB per concurrent flow, with a 1 vCPU and 2 GB API tier in front, which is where this install's 4 GB floor comes from. source
- Upstream's guidance is no sandboxing for a single-tenant install, which is also the image default, so flow code runs with the container's own privileges. source
- Without SMTP configured there is no password-reset email, and the documented recovery is updating a bcrypt hash directly in the user_identity table. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host Zapier?
Not Zapier itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Activepieces. Trigger-and-action automation with a visual builder, run on your own box, with no task meter. The install is one evening: 3 containers behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 105 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces Zapier?
Activepieces. Trigger-and-action automation with a visual builder, run on your own box, with no task meter. The closest thing to a like-for-like swap that you can actually run on one small server. It keeps the shape people are paying for, a visual builder with triggers, actions and branching plus a large catalogue of prebuilt pieces, and it drops the task meter entirely: the community edition is uncapped on runs, users and flows. What you take on is the half Zapier was really selling. Your own OAuth registration for each service you connect, your own encryption key standing between a backup and every token in it, and flow code that runs unsandboxed with your container's network reach. Three containers, one evening, and the bill stops. Activepieces is MIT-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to Zapier?
4096 MB of RAM and 20 GB of disk — the smallest tier most VPS hosts sell, about $20 a month. Activepieces itself is free and MIT-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Zapier Professional, $29.99/mo — $359.88 a year, a metered rate, not a whole bill.
How hard is it really?
ONE EVENING — 1–3 hours. The rule that produced that verdict: up to three containers and at most one outside integration. You will type more than one command and read a page of documentation, and it will be running before you go to bed. The tier is derived from seven countable facts about the Activepieces install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run Activepieces on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same Activepieces on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: Webhook triggers need a URL the outside world can reach, and on this path every URL begins with http://localhost:8095, so what you get is the scheduled and polling half of the tool, running only while the computer is awake. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-06. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.