26 min read

Self-Host OpenTag with Docker Compose and Localtonet

Install and verify OpenTag with Docker Compose, pair its Runner, then provide public HTTPS ingress for Slack through a Localtonet HTTP tunnel.

Self-hosted OpenTag stack receiving Slack traffic through a Localtonet HTTPS origin and HTTP tunnel.
Slack reaches the self-hosted OpenTag Control Plane through a Localtonet public HTTPS endpoint.
Self-Hosting ยท OpenTag ยท Localtonet ยท 2026

Run an AI coding teammate on infrastructure you control, then connect Slack to it through a public HTTPS origin

OpenTag combines a self-hosted Control Plane with a paired Runner that invokes an ACP coding agent against a local checkout. This guide installs the Control Plane with Docker Compose, prepares its required configuration and file-backed secrets, pairs the Runner, and verifies the deployment with the OpenTag CLI. After the local installation works, we explain how to place a Localtonet HTTP tunnel in front of the Control Plane so Slack can reach it without inbound router port forwarding, firewall changes, a VPN, or a public IP address. Where the available OpenTag evidence does not establish an exact port, health path, secret filename, or Slack callback path, we identify that boundary rather than guessing.

๐Ÿ”’ Credentials remain on their intended Control Plane or Runner ๐ŸŒ Public HTTPS ingress through a Localtonet HTTP tunnel โšก Docker Compose installation with CLI-based Runner verification

How the OpenTag deployment works

OpenTag is designed as a persistent AI teammate in Slack. A request begins when someone mentions the configured OpenTag identity in a Slack channel. The self-hosted Control Plane receives and coordinates that request, while a paired Runner performs the actual work on a computer that has the approved local checkout and coding-agent environment.

This separation is important. The Control Plane is the continuously reachable coordination layer. It stores coordination state and uses PostgreSQL for its self-hosted deployment. The Runner is the execution owner. It launches the configured coding agent through ACP and works against the local Project Target. OpenTag then returns status, decisions, blockers, and evidence to the originating Slack thread.

๐Ÿงญ Self-hosted Control Plane The Control Plane receives Slack requests and owns the canonical Run, Attempt, lease, approval, delivery journal, and terminal assessment.
๐Ÿ’ป Paired Runner One paired Runner executes work on a user-controlled computer where the local checkout and approved coding-agent environment already exist.
๐Ÿ—„๏ธ PostgreSQL state The Docker Compose deployment includes PostgreSQL through its Compose profile, providing the database required by the self-hosted Control Plane.
๐Ÿ’ฌ Slack request source Slack is the supported Source App in the documented team profile. Requests, queue state, approvals, blockers, and results remain associated with the originating thread.
๐Ÿ”ง ACP coding agent The Runner starts the configured coding agent through ACP while keeping raw tool output out of the team conversation.
๐ŸŒ Public HTTPS origin Slack must be able to reach the Control Plane through a public HTTPS origin. A Localtonet HTTP tunnel can provide that origin after the local service is confirmed working.

OpenTag deliberately keeps sensitive material in separate trust zones. The source code, local checkout and worktrees, coding-agent login or session, and GitHub credential stay on the Runner. Slack credentials and the Control Plane service credentials stay on the Control Plane. The Control Plane coordinates work, but it is not intended to become a copy of the development computer.

The documented product path is intentionally narrow: Slack reaches the self-hosted Control Plane, the Control Plane coordinates one paired Runner, the Runner invokes one ACP coding agent, and an optional GitHub draft pull request or evidence result can return to the Slack thread. GitHub is a Project Target and optional publication or evidence provider. It is not a second request inbox in this workflow.

OpenTag does not use a standalone local mode

The documented paired-only runtime requires a trusted self-hosted Control Plane URL and a bootstrap pairing credential. The Runner must be paired with that Control Plane. Do not plan the deployment around a local_direct compatibility mode, because the current documented workflow does not provide one.

Prerequisites and deployment decisions

Prepare the Control Plane host, Runner computer, credentials, and networking path before starting. The Control Plane and Runner can serve different operational roles, so they do not have to be treated as one machine. The Localtonet client must ultimately run on a device that can reach the local Control Plane address and port.

Requirement Where it is needed Purpose
Git Control Plane host Clones the OpenTag repository containing the supported Docker Compose deployment.
Docker with Docker Compose Control Plane host Builds and starts the Control Plane stack and its included PostgreSQL deployment.
Writable local storage Control Plane host Stores the repository checkout, environment file, file-backed secrets, and Docker-managed data.
Slack configuration and credentials Control Plane Allows the supported Slack Source App to communicate with the self-hosted presence.
Relay-content and service credentials Control Plane Satisfies the placeholders and file-backed secret requirements documented by the Compose deployment.
Node.js and npm environment Runner computer Installs the documented global @opentag/cli package.
Approved local project checkout Runner computer Provides the Project Target against which the ACP coding agent works.
Configured ACP coding agent Runner computer Performs the coding work initiated through OpenTag.
Public HTTPS origin Ingress to the Control Plane Gives Slack an internet-reachable HTTPS endpoint for the required integration traffic.
Localtonet client and device token A device that can reach the Control Plane Establishes the outbound connection to our relay and publishes the selected local HTTP target.

Choose the Control Plane host

The Control Plane host needs to run the Docker Compose stack reliably. Because it is the persistent Slack-facing presence, use a machine that can remain available independently of the Runner whenever practical. If the Runner goes offline, the Control Plane can still represent the request as queued rather than disappearing with the development computer.

Choose the Runner computer

The Runner belongs on the computer where the approved source checkout, coding-agent login or session, and any required GitHub credential should remain. Pairing should be performed by an operator who understands which local project the Runner will expose to the OpenTag workflow.

Decide where the Localtonet client will run

Our client can run on the Control Plane host or another device that can reach the Control Plane over the local network. The HTTP tunnel forwards to a local IP address and port reachable from that client device. Running both on one host often simplifies reachability, but it is not mandatory.

Do not proceed with unresolved placeholders

The OpenTag quick-start instructions require every placeholder in .env to be replaced and the documented file-backed Slack and relay-content secrets to be created before starting the stack. The supplied evidence does not establish the exact current secret filenames or every environment variable, so this article does not invent them. Treat the repository's checked-out deploy/compose/.env.example and Compose guide for the exact revision you cloned as authoritative.

Install the OpenTag Control Plane with Docker Compose

Compose file, running OpenTag container, and local service verification shown in a desktop development view.
Docker Compose defines and starts the OpenTag Control Plane on the local host.

The supported quick-start path clones the OpenTag repository, enters its Compose deployment directory, copies the environment template, completes the configuration and file-backed secrets, and then builds and starts the stack. Perform these commands on the machine selected for the Control Plane.

1

Clone the OpenTag repository

Obtain the repository containing the Control Plane and its Docker Compose deployment.

git clone https://github.com/amplifthq/opentag.git
2

Enter the Compose deployment directory

Work from the repository's documented Compose directory so the expected Compose and environment files are in context.

cd opentag/deploy/compose
3

Create the local environment file

Copy the supplied example instead of creating an unrelated environment file from memory.

cp .env.example .env
4

Replace placeholders and create file-backed secrets

Open .env, replace every placeholder, and create the Slack and relay-content secret files required by the Compose guide for the exact revision you checked out. Keep the values out of shell history, screenshots, chat messages, tickets, and source control. Make sure the active Slack binding uses a known OPENTAG_SLACK_PROJECT_TARGET_ID, because the Runner setup must later receive that exact value.

5

Build and start the Compose stack

Start the self-hosted presence using the completed environment file. This command builds the required images and starts the Compose deployment.

docker compose --env-file .env up --build

Keep the startup output visible during the first launch. Configuration errors, missing secret files, unavailable dependencies, and database startup failures are generally easier to diagnose before adding public ingress. Do not create the Localtonet tunnel merely to compensate for an application that has not started locally.

Identify the published local target without guessing

To configure an HTTP tunnel, you need the Control Plane's local address and published host port. You may also want an application-specific health path for local testing. Those values are not established in the supplied OpenTag evidence, so this guide cannot responsibly provide a numeric port or URL path.

Read the checked-out Compose configuration and its accompanying deployment guide for the exact revision you installed. Identify the Control Plane service's published host port, not only its container-internal port. If the service is bound only inside Docker's private network and no host port is published, a Localtonet client running directly on the host cannot target that private container address through a normal host-local HTTP target. Follow OpenTag's documented deployment arrangement rather than adding an arbitrary port mapping.

Do not guess the OpenTag port or health endpoint

A plausible-looking port or path can point the tunnel at the wrong process, expose an unintended administrative service, or produce misleading verification results. Record the actual host address, published port, and supported verification path from the deployed Compose revision before continuing.

Install and pair the OpenTag Runner

Three-stage flow showing the OpenTag Runner pairing with the Control Plane and reaching connected status.
Pairing establishes the Runner relationship that should be verified before processing work.

Pair the Runner only after the Control Plane is available through its trusted URL and the required bootstrap pairing credential is ready. If Slack must reach the same public origin used for pairing, establish and test the Localtonet tunnel before running setup. If an existing trusted origin is already available, pairing can use that origin and the tunnel can be introduced according to your deployment plan.

The current extracted OpenTag quick start pins the CLI installation to version 0.11.0. Use that exact command for this documented workflow rather than silently mixing it with an older release. If you intentionally deploy a different OpenTag revision, confirm its CLI compatibility before changing the pinned version.

1

Install the documented OpenTag CLI version

Run the global npm installation on the computer that will act as the Runner.

npm install -g @opentag/cli@0.11.0
2

Run interactive setup against the trusted Control Plane

Replace the example relay origin with the actual trusted HTTPS origin for your self-hosted Control Plane. The setup process configures and pairs one Runner, one GitHub Project Target, and one ACP executor.

opentag setup --relay https://relay.example.com

The hostname shown above is a placeholder from the documented command shape. Do not use it literally. When prompted, provide the required bootstrap pairing credential and enter the exact OPENTAG_SLACK_PROJECT_TARGET_ID used by the active Slack binding. Setup registers that target through the Runner credential and verifies the Control Plane readback before pairing completes.

3

Start the paired Runner

Start the Runner after setup has completed successfully.

opentag start
Use opentag pair for its documented recovery purpose

The normal first-time workflow uses opentag setup. Use opentag pair only to finish an interrupted pairing or to pair an existing unpaired configuration. It is not the replacement for completing the initial setup of the Runner, Project Target, and ACP executor.

Do not reuse pre-reset Runner state implicitly

If this computer ran a pre-reset OpenTag checkout, point OPENTAG_CONFIG_HOME and OPENTAG_STATE_DIR at new empty directories before setup. The paired Runner does not reinterpret or rewrite an earlier configuration or SQLite database. Preserve old data separately until the new pairing has been verified.

Verify the Control Plane and Runner before real work

Verification should proceed in layers. First confirm that the Docker Compose deployment remains running without an unresolved configuration or dependency failure. Next verify the local HTTP target using the address, published port, and supported path identified from the checked-out deployment. Finally use the OpenTag CLI to confirm the paired Runner configuration and status.

Check the local service boundary

Test the Control Plane directly from the Localtonet client device before testing the public URL. If the client runs on the same host, use the exact host-local address and published port established by the Compose deployment. If it runs on another LAN device, use an address that the client can actually route to and confirm that host-level access controls allow only the intended local path.

The available evidence does not define the Control Plane's listening port, health-check path, or expected HTTP status. For that reason, this article does not provide a fabricated curl command. Use the endpoint documented by the exact Compose revision. A successful local response demonstrates application reachability, while an application-specific health result may also validate dependencies such as PostgreSQL. Do not assume those two checks are equivalent unless OpenTag documents them as equivalent.

Run the supported CLI checks

Before sending the first real Slack request, run both documented verification commands on the Runner:

opentag doctor
opentag status

opentag doctor is the installation diagnostic check, while opentag status reports the Runner's current OpenTag status. Treat failures as setup issues to resolve before asking a team to depend on the integration. Verify the configured Control Plane origin, pairing state, Project Target, and ACP executor inputs rather than repeatedly restarting an unknown configuration.

Understand the presence result

Presence state Meaning Operator interpretation
available The Slack binding, Project Target, Runner, and fresh readiness all exist. The documented components needed to accept work are present.
queued A request is durably waiting for the paired Runner. Check Runner availability and readiness rather than resubmitting the same request blindly.
working The current fenced Attempt is assigned or running on a ready Runner. Work is active. Avoid creating duplicate work merely because completion is not immediate.
needs_attention A decision, reconciliation, or conflicting active work needs a human. Review the exact action requested by OpenTag before allowing the workflow to continue.
offline The binding exists, but the Runner has no fresh readiness receipt. Check the Runner process, its network path, and the paired configuration.
setup_required The Slack binding, Project Target, or Runner is incomplete. Return to configuration and pairing rather than treating it as a tunnel-only problem.

These presence states are projections of existing facts. They are not commands and cannot claim, retry, cancel, or settle work. This distinction matters during troubleshooting because a displayed state should not be treated as evidence that an operator action already occurred.

Publish the OpenTag Control Plane with a Localtonet HTTP tunnel

Once the Control Plane works locally, Localtonet can provide its public internet entry point. Our client establishes an outbound connection from the selected device to a Localtonet relay server. The resulting HTTP tunnel forwards the public HTTPS address to the Control Plane's local IP address and port, without requiring inbound router port forwarding, firewall changes, VPN setup, or a public IP address.

This is standard HTTP tunneling, not VPN functionality. Localtonet VPN Manager is a separate private mesh VPN feature. For Slack ingress to a local web service, use an HTTP tunnel that targets the verified Control Plane endpoint.

1

Install and run the Localtonet client

Install our client on the Control Plane host or another device that can reach the verified local Control Plane address and port. Keep the client running because the tunnel is available only while the selected device is connected and the tunnel is running.

2

Authenticate or select the client device

Use the device-specific authentication token associated with the client that will run the tunnel. Do not copy the token into this article, source control, public logs, or Slack. Tokens identify individual devices and must not be guessed or exposed.

3

Select an available relay server

Choose a currently available Localtonet relay server or region from the dashboard. Available server codes can vary, so obtain the current value from our product instead of hardcoding a region from an unrelated example.

4

Create an HTTP tunnel to the verified local target

Select the HTTP tunnel family and enter the exact local IP address and published host port discovered from the OpenTag Compose deployment. Choose an available Process Type such as a random subdomain, selected subdomain where supported, or custom domain. These options serve the local content through a public HTTPS address. Custom-domain DNS details should be taken from the current Localtonet documentation rather than inferred.

5

Start the tunnel

Creating a tunnel does not start it. Use the Start control after reviewing the target. The dashboard will then provide the assigned public URL when the selected client is connected and the tunnel is running.

6

Test the public HTTPS origin

Open or request the assigned HTTPS origin using the OpenTag-supported path you already verified locally. Confirm that the response belongs to the intended Control Plane service. After this public test succeeds, use the exact origin where OpenTag and Slack require it.

For current product controls and setup details, consult our Localtonet documentation. Everything can be managed from the Localtonet dashboard or REST API, but this guide intentionally avoids inventing API requests or client commands that are not present in the supplied product context.

A public URL expands the service's exposure boundary

Publish only the OpenTag HTTP endpoint required for the Slack integration. Do not point the tunnel at PostgreSQL, Docker management interfaces, host administration tools, development dashboards, or an unspecified container port. Protect the Control Plane with its documented credentials and request-validation mechanisms, apply least privilege to Slack and GitHub access, and stop or delete the tunnel when public ingress is no longer required.

HTTPS is provided at the tunnel edge

HTTP and File Server Process Types serve content at a public HTTPS address. The tunnel then forwards to the configured local HTTP target. This statement does not imply an unsupported end-to-end encryption mode, compliance certification, availability guarantee, or plan entitlement.

Connect Slack to the public origin safely

Slack webhook path through a public HTTPS origin and Localtonet tunnel to a private OpenTag deployment.
Slack uses the public HTTPS origin while the OpenTag services remain on the private host.

Slack is the Source App for this documented OpenTag team profile. Configure the Slack integration using the exact callback, event, or request paths required by the OpenTag Compose guide for the installed revision. The available evidence confirms that Slack needs a public HTTPS origin but does not provide the exact Slack dashboard fields or URL paths, so those values must not be improvised.

Keep the origin stable for as long as Slack depends on it. If a generated Localtonet address changes because a tunnel is recreated or its Process Type changes, update every OpenTag and Slack setting that references the old origin. A custom subdomain or custom domain may offer a more stable naming workflow where supported, but availability can vary by plan and current product configuration. Check the dashboard before relying on a specific option.

Use an end-to-end verification sequence

After the public endpoint is configured, test the complete workflow with a small, non-destructive request in the intended Slack channel. A representative request format is:

@OpenTag investigate the failing check and propose a fix

Observe whether the request appears in the Control Plane, whether the Runner becomes active, and whether status returns to the same thread. Begin with investigation or proposal work rather than a request that changes production systems. If publication to GitHub is enabled, use a repository and permissions appropriate for controlled testing.

OpenTag separates executor output, Run state, GitHub publication, and provider delivery into distinct facts with distinct evidence. A coding agent reporting success does not by itself establish that a pull request exists, checks passed, or Slack received the final delivery. Preserve this separation during acceptance testing.

Respect governed actions

Draft pull request publication is a distinct governed stage. OpenTag does not claim that chat text can broaden access or automatically merge changes. If work requires a decision, the Slack thread should identify the action that needs attention. A provider timeout or ambiguous side effect can remain outcome_unknown rather than being reported as a fabricated success or replayed blindly.

Never place credentials in the public origin

Do not include Slack secrets, bootstrap pairing credentials, device auth tokens, GitHub credentials, or Control Plane service credentials in URLs, commands shared with others, screenshots, or troubleshooting messages. The public HTTPS address is an ingress location, not a credential transport mechanism.

Routine operations and troubleshooting

Operate the deployment as three connected but independently testable layers: the OpenTag Control Plane stack, the paired Runner, and the Localtonet HTTP tunnel. This separation prevents every failure from being misdiagnosed as an ingress problem.

Recommended operating checks

After configuration changes or maintenance, confirm that the Compose stack starts with its intended .env and file-backed secrets. Verify the local Control Plane endpoint from the Localtonet client device. Run opentag doctor and opentag status on the Runner. Then verify that the Localtonet client is connected, the HTTP tunnel is running, and the public origin reaches the same intended service.

Finally, issue a controlled Slack request and watch its progression from the thread to the Control Plane and Runner. This sequence gives each layer a clear pass or fail boundary.

The Compose stack does not start

Return to deploy/compose/.env.example and compare it with the active .env. Look for untouched placeholders, missing file-backed Slack or relay-content secrets, incorrect file locations, and service credentials that do not match the current checked-out deployment guide. Also confirm that Docker Compose is available and that the included PostgreSQL service can start.

Do not solve a startup failure by opening additional network ports. Public ingress cannot repair an application that fails before it listens.

The service works on the host but not from the Localtonet client

Check whether the Localtonet client is running on the same host or another device. An address that resolves to the Control Plane host's loopback interface is local only to that host. If our client runs elsewhere, configure the tunnel with a local network address that the client device can reach, subject to your network policy.

Confirm that you selected the published host port rather than a Docker-internal port. Also ensure the target is the Control Plane's HTTP service and not PostgreSQL or another container.

The public Localtonet URL is unavailable

Confirm that the selected client device is connected and that the tunnel was explicitly started. A saved tunnel configuration is not automatically a running tunnel. Verify the selected device token, current relay selection, local target IP, and local target port. If the local endpoint fails from the client device, fix that local path before testing the public address again.

The public origin responds, but Slack requests fail

A successful browser response proves only that some HTTP route is reachable. Recheck the exact OpenTag callback or request path required by the installed revision, the Slack-side configuration, and the Control Plane's Slack credentials. Confirm that the request is reaching the intended OpenTag service rather than a default route or unrelated web application.

Because the supplied evidence does not define Slack's exact configuration fields or OpenTag's request path, use the checked-out Compose guide for those values. Do not substitute a guessed route.

The Runner reports setup_required

Verify that the Slack binding, GitHub Project Target, and Runner pairing are complete. During setup, the entered Project Target identifier must exactly match OPENTAG_SLACK_PROJECT_TARGET_ID from the active Slack binding. Recheck the trusted Control Plane origin and bootstrap pairing process.

The Runner is offline or work remains queued

Confirm that opentag start is running on the paired computer and that the machine can reach the trusted Control Plane origin. Then run opentag doctor and opentag status. Do not create duplicate Slack requests simply because the Runner was temporarily unavailable. OpenTag is designed to keep work visibly queued while the Runner is offline.

Pairing fails after an earlier reset or checkout

Do not expect the paired Runner to reinterpret an old configuration or SQLite database. Set OPENTAG_CONFIG_HOME and OPENTAG_STATE_DIR to new empty directories, then repeat the documented setup path. Use opentag pair only when finishing interrupted pairing or pairing an existing unpaired configuration.

A result appears successful but publication is uncertain

Treat execution, Run state, GitHub publication, observed checks, and provider delivery as separate facts. If the system reports outcome_unknown, investigate the provider state before retrying an action that may already have happened. This reduces the risk of duplicate side effects.

Stopping or changing public access

Stop the Localtonet tunnel when Slack ingress should no longer be available. Delete the tunnel if the configuration is obsolete. If you change the public origin, update the OpenTag and Slack configuration that depends on it, then repeat local, public, CLI, and Slack-level verification.

Keep version changes deliberate

The extracted quick start installs @opentag/cli@0.11.0. Do not assume that commands, environment variables, schemas, or Compose behavior from another OpenTag release are interchangeable. Review the checked-out revision's configuration template and deployment guide before upgrading the Control Plane or CLI.

Frequently asked questions

Does OpenTag require a public HTTPS URL?

Yes. The documented self-hosted presence requires a public HTTPS origin that Slack can reach. A Localtonet HTTP tunnel can provide that origin by forwarding a public HTTPS address to the verified local Control Plane IP address and port.

What port does the OpenTag Control Plane use?

The supplied evidence does not establish a numeric listening port or published host port. Read the Compose configuration and deployment guide from the exact OpenTag revision you cloned, then use the published host port as the Localtonet HTTP target. Do not guess.

What health-check path should I use?

No health-check path is established in the available evidence. Use the application endpoint documented by the checked-out OpenTag Compose revision. Verify it locally before using the same supported path through the public tunnel.

Must the Localtonet client run on the same machine as OpenTag?

No. It can run on the Control Plane host or another device that can reach the Control Plane's local IP address and port. The selected client must remain connected while the tunnel is running.

Is the Localtonet HTTP tunnel a VPN?

No. An HTTP tunnel publishes the selected local web service through a public URL. Localtonet VPN Manager is the separate private mesh VPN feature.

Can OpenTag continue receiving work while the Runner is offline?

The self-hosted Control Plane remains the persistent presence. A request can remain visibly queued while the paired Runner is offline. The Runner must become ready before it can execute the queued work.

Does OpenTag copy my source code to the Control Plane?

The documented architecture keeps the source code, local checkout and worktrees, coding-agent session, and GitHub credential on the Runner. The Control Plane keeps coordination state and its own Slack and service credentials rather than acting as a copy of the development computer.

Can I use multiple Runners or Source Apps in this workflow?

The current documented team profile is deliberately narrow: Slack is the Source App and one paired Runner is the execution owner. The repository does not currently claim multi-Runner scheduling or other Source Apps for this path.

Does creating a Localtonet tunnel immediately make it available?

No. Creating the configuration does not start the tunnel. You must use the Start control, and the selected client device must be connected. You can later stop or delete the tunnel.

Which OpenTag CLI version does this guide install?

This workflow uses the version pinned by the extracted current quick start: @opentag/cli@0.11.0. If you deploy another Control Plane revision, verify its required CLI version instead of assuming compatibility.

Give your OpenTag Control Plane a public HTTPS origin

After OpenTag works locally and the Runner passes its checks, create a Localtonet HTTP tunnel to the verified Control Plane address and port. Keep credentials private, expose only the required application endpoint, and validate the full Slack-to-Runner workflow before inviting broader use.

Get Started Free โ†’

Localtonet is a secure multi-protocol tunneling and proxy platform designed to expose localhost, devices, private services, and AI agents to the public internet supporting HTTP/HTTPS tunnels, TCP/UDP forwarding, mobile proxy infrastructure, file server publishing, latency-optimized game connectivity, and developer-ready AI agent endpoint exposure from a single unified control plane.

support