diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
index cdaf7094d917d90da6e6b849dbceb74aed64720f..f76410529e95f78ff31eb806379c7ef970d8dff2 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
@@ -30,6 +30,7 @@ import dagger.hilt.components.SingletonComponent
 import org.briarproject.mailbox.core.event.EventBus
 import org.briarproject.mailbox.core.lifecycle.IoExecutor
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.server.WebServerManager
 import org.briarproject.mailbox.core.settings.SettingsManager
 import org.briarproject.mailbox.core.system.AndroidWakeLockManager
 import org.briarproject.mailbox.core.system.Clock
@@ -74,6 +75,7 @@ internal class AndroidTorModule {
         androidWakeLockManager: AndroidWakeLockManager,
         lifecycleManager: LifecycleManager,
         eventBus: EventBus,
+        webServerManager: WebServerManager,
     ): TorPlugin = AndroidTorPlugin(
         ioExecutor,
         app,
@@ -85,8 +87,8 @@ internal class AndroidTorModule {
         circumventionProvider,
         androidWakeLockManager,
         architecture,
-        app.getDir("tor", Context.MODE_PRIVATE),
-    ).also {
+        app.getDir("tor", Context.MODE_PRIVATE)
+    ) { webServerManager.port }.also {
         lifecycleManager.registerService(it)
         eventBus.addListener(it)
     }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
index c3e30ca7899531ad78414e1f90f6ca7b8a856e98..92992fb3edf8dc2622296c583d6e92b93d124629 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
@@ -42,6 +42,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.Executor;
+import java.util.function.IntSupplier;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
@@ -76,10 +77,11 @@ public class AndroidTorPlugin extends AbstractTorPlugin {
 			CircumventionProvider circumventionProvider,
 			AndroidWakeLockManager wakeLockManager,
 			@Nullable String architecture,
-			File torDirectory) {
+			File torDirectory,
+			IntSupplier portSupplier) {
 		super(ioExecutor, settingsManager, networkManager, locationUtils, clock,
 				resourceProvider, circumventionProvider, architecture,
-				torDirectory);
+				torDirectory, portSupplier);
 		this.ctx = ctx;
 		wakeLock = wakeLockManager.createWakeLock("TorPlugin");
 		String nativeLibDir = ctx.getApplicationInfo().nativeLibraryDir;
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
index 3f5114ac0b6298fbddc1a7765de7fdd3fe5d4e95..e9a40f6d97e8396f0f83d82c1705c385b6462290 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
@@ -27,10 +27,10 @@ import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
 import io.ktor.server.plugins.callloging.CallLogging
 import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
+import kotlinx.coroutines.runBlocking
 import org.briarproject.mailbox.core.contacts.ContactsManager
 import org.briarproject.mailbox.core.files.FileRouteManager
 import org.briarproject.mailbox.core.lifecycle.Service
-import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
 import org.briarproject.mailbox.core.settings.MetadataRouteManager
 import org.briarproject.mailbox.core.setup.SetupRouteManager
 import org.briarproject.mailbox.core.setup.WipeRouteManager
@@ -38,9 +38,10 @@ import javax.inject.Inject
 import javax.inject.Singleton
 
 interface WebServerManager : Service {
-    companion object {
-        const val PORT: Int = 8000
-    }
+    /**
+     * Accessing this can block the current thread until the port chosen by the webserver is known.
+     */
+    val port: Int
 }
 
 @Singleton
@@ -54,7 +55,7 @@ internal class WebServerManagerImpl @Inject constructor(
 ) : WebServerManager {
 
     private val server by lazy {
-        embeddedServer(Netty, PORT, watchPaths = emptyList()) {
+        embeddedServer(Netty, 0, watchPaths = emptyList()) {
             install(CallLogging)
             install(Authentication) {
                 bearer {
@@ -73,6 +74,7 @@ internal class WebServerManagerImpl @Inject constructor(
             configureFilesApi(fileRouteManager)
         }
     }
+    override val port get() = runBlocking { server.resolvedConnectors().first().port }
 
     override fun startService() {
         server.start()
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
index ad17c5d9eaa5e6c9aed384e830abea5ae989abff..379b19f9bf3d99d38c2ee561351a2dcbaad38ea4 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/AbstractTorPlugin.java
@@ -29,7 +29,6 @@ import org.briarproject.mailbox.core.event.Event;
 import org.briarproject.mailbox.core.event.EventListener;
 import org.briarproject.mailbox.core.lifecycle.IoExecutor;
 import org.briarproject.mailbox.core.lifecycle.ServiceException;
-import org.briarproject.mailbox.core.server.WebServerManager;
 import org.briarproject.mailbox.core.settings.Settings;
 import org.briarproject.mailbox.core.settings.SettingsManager;
 import org.briarproject.mailbox.core.system.Clock;
@@ -55,6 +54,7 @@ import java.util.Map;
 import java.util.Scanner;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.IntSupplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.ZipInputStream;
@@ -113,7 +113,7 @@ public abstract class AbstractTorPlugin
 	 */
 	private static final int HS_DESC_UPLOADS = 1;
 	private final Pattern bootstrapPattern =
-			Pattern.compile("^Bootstrapped ([0-9]{1,3})%.*$");
+			Pattern.compile("^Bootstrapped (\\d{1,3})%.*$");
 	private final Pattern clockSkewPattern = Pattern.compile("CLOCK_SKEW");
 
 	private final Executor ioExecutor;
@@ -128,6 +128,7 @@ public abstract class AbstractTorPlugin
 	private final ResourceProvider resourceProvider;
 	private final File torDirectory, configFile;
 	private final File doneFile, cookieFile;
+	private final IntSupplier portSupplier;
 	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	protected final PluginState state = new PluginState();
@@ -147,7 +148,8 @@ public abstract class AbstractTorPlugin
 			ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			@Nullable String architecture,
-			File torDirectory) {
+			File torDirectory,
+			IntSupplier portSupplier) {
 		this.ioExecutor = ioExecutor;
 		this.settingsManager = settingsManager;
 		this.networkManager = networkManager;
@@ -160,6 +162,7 @@ public abstract class AbstractTorPlugin
 		configFile = new File(torDirectory, "torrc");
 		doneFile = new File(torDirectory, "done");
 		cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
+		this.portSupplier = portSupplier;
 		// Don't execute more than one connection status check at a time
 		connectionStatusExecutor =
 				new PoliteExecutor("TorPlugin", ioExecutor, 1);
@@ -264,8 +267,16 @@ public abstract class AbstractTorPlugin
 		// Check whether we're online
 		updateConnectionStatus(networkManager.getNetworkStatus());
 		// Create a hidden service if necessary
-		ioExecutor.execute(() -> publishHiddenService(
-				String.valueOf(WebServerManager.PORT)));
+		ioExecutor.execute(() -> {
+			int port;
+			try {
+				port = portSupplier.getAsInt();
+			} catch (Exception e) {
+				throw new AssertionError(e);
+			}
+			info(LOG, () -> "Binding hidden service to port: " + port);
+			publishHiddenService(String.valueOf(port));
+		});
 	}
 
 	private boolean assetsAreUpToDate() {
@@ -290,6 +301,7 @@ public abstract class AbstractTorPlugin
 	}
 
 	protected void extract(InputStream in, File dest) throws IOException {
+		@SuppressWarnings("IOStreamConstructor") // not in Java 6 minSdk 16
 		OutputStream out = new FileOutputStream(dest);
 		copyAndClose(in, out);
 	}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
index 960874cd8156d7c84da414d9c281237a3510881a..f662e6fb20ddc5b823199f92bab6219ed32c6085 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt
@@ -6,6 +6,7 @@ import org.briarproject.mailbox.core.db.DatabaseConfig
 import org.briarproject.mailbox.core.files.FileManager
 import org.briarproject.mailbox.core.files.FileProvider
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.server.WebServerManager
 import org.briarproject.mailbox.core.settings.MetadataManager
 import org.briarproject.mailbox.core.settings.SettingsManager
 import org.briarproject.mailbox.core.setup.SetupManager
@@ -28,5 +29,6 @@ interface TestComponent {
     fun getDatabase(): Database
     fun getFileProvider(): FileProvider
     fun getMetadataManager(): MetadataManager
+    fun getWebServerManager(): WebServerManager
     fun getWipeManager(): WipeManager
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
index d1365003964f9e05559f9524104d48f661541a72..dee124dc9797296b3d0d5e78f1228da9a60c7e5f 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
@@ -15,7 +15,6 @@ import org.briarproject.mailbox.core.TestModule
 import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
 import org.briarproject.mailbox.core.TestUtils.getNewRandomId
 import org.briarproject.mailbox.core.contacts.Contact
-import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
 import org.junit.jupiter.api.AfterAll
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeAll
@@ -54,11 +53,11 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) {
             level = LogLevel.ALL
         }
     }
-    protected val baseUrl = "http://127.0.0.1:$PORT"
+    protected lateinit var baseUrl: String
+        private set
 
     protected val ownerToken = getNewRandomId()
     protected val token = getNewRandomId()
-    protected val id = getNewRandomId()
     protected val contact1 = getNewRandomContact()
     protected val contact2 = getNewRandomContact()
     protected var tempDir: File? = null
@@ -83,6 +82,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) {
         assertFalse(setupManager.hasDb)
         lifecycleManager.startServices()
         lifecycleManager.waitForStartup()
+        baseUrl = "http://127.0.0.1:${testComponent.getWebServerManager().port}"
     }
 
     @AfterAll
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
index 0885c267ff1dacfe3b32ca40d424827e6fe8caf8..785764f2f10560f10c1c8c1598ba6eddd7a83458 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
@@ -22,7 +22,6 @@ import io.ktor.server.response.respond
 import io.ktor.server.routing.post
 import io.ktor.server.routing.routing
 import kotlinx.coroutines.runBlocking
-import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
 
@@ -43,7 +42,7 @@ class WebServerIntegrationTest : IntegrationTest() {
 
     @Test
     fun testJacksonUnsafeDeserialization(): Unit = runBlocking {
-        val port = PORT + 1
+        val port = 8000
         val server = embeddedServer(Netty, port, watchPaths = emptyList()) {
             install(CallLogging)
             install(ContentNegotiation) {
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
index 904f84f13efc5ea4849783c2db2233ebd80ed8a1..e74c78256ca62ef8c3cde13740addd74401f0f0c 100644
--- a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
@@ -27,6 +27,7 @@ import org.briarproject.mailbox.core.event.EventBus
 import org.briarproject.mailbox.core.files.FileProvider
 import org.briarproject.mailbox.core.lifecycle.IoExecutor
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
+import org.briarproject.mailbox.core.server.WebServerManager
 import org.briarproject.mailbox.core.settings.SettingsManager
 import org.briarproject.mailbox.core.system.Clock
 import org.briarproject.mailbox.core.system.LocationUtils
@@ -67,6 +68,7 @@ class JavaTorModule {
         lifecycleManager: LifecycleManager,
         eventBus: EventBus,
         fileProvider: FileProvider,
+        webServerManager: WebServerManager,
     ): TorPlugin {
         val torDir = File(fileProvider.root, "tor")
         return JavaTorPlugin(
@@ -78,8 +80,8 @@ class JavaTorModule {
             resourceProvider,
             circumventionProvider,
             architecture,
-            torDir,
-        ).also {
+            torDir
+        ) { webServerManager.port }.also {
             lifecycleManager.registerService(it)
             eventBus.addListener(it)
         }
diff --git a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
index 72eef3529e9c8ab67d131e7a0600bc89a32ff5d5..6681285fecf551ed5ad66518d900a53951ec9ef1 100644
--- a/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
+++ b/mailbox-lib/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java
@@ -32,6 +32,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.CodeSource;
 import java.util.concurrent.Executor;
+import java.util.function.IntSupplier;
 
 import javax.annotation.Nullable;
 
@@ -45,10 +46,11 @@ public class JavaTorPlugin extends AbstractTorPlugin {
 			ResourceProvider resourceProvider,
 			CircumventionProvider circumventionProvider,
 			@Nullable String architecture,
-			File torDirectory) {
+			File torDirectory,
+			IntSupplier portSupplier) {
 		super(ioExecutor, settingsManager, networkManager, locationUtils, clock,
 				resourceProvider, circumventionProvider, architecture,
-				torDirectory);
+				torDirectory, portSupplier);
 	}
 
 	@Override